Skip to main content

codex_exec_server/
client.rs

1use std::collections::BTreeMap;
2use std::collections::HashMap;
3use std::sync::Arc;
4use std::sync::Mutex as StdMutex;
5use std::sync::OnceLock;
6use std::sync::atomic::AtomicBool;
7use std::sync::atomic::AtomicU64;
8use std::sync::atomic::Ordering;
9use std::time::Duration;
10
11use arc_swap::ArcSwap;
12use codex_exec_server_protocol::JSONRPCNotification;
13use futures::FutureExt;
14use futures::future::BoxFuture;
15use serde_json::Value;
16use tokio::sync::Mutex;
17use tokio::sync::OnceCell;
18use tokio::sync::Semaphore;
19use tokio::sync::mpsc;
20use tokio::sync::watch;
21use tokio_util::task::AbortOnDropHandle;
22
23use tokio::time::timeout;
24use tracing::Instrument;
25use tracing::debug;
26use tracing::instrument::WithSubscriber;
27
28use crate::ProcessId;
29use crate::client::http_client::response_body_stream::MAX_QUEUED_HTTP_BODY_BYTES;
30use crate::client::http_client::response_body_stream::QueuedHttpBodyDelta;
31use crate::client_api::ExecServerClientConnectOptions;
32use crate::client_api::ExecServerTransportParams;
33use crate::client_api::HttpClient;
34use crate::client_api::RemoteExecServerConnectArgs;
35use crate::client_api::StdioExecServerConnectArgs;
36use crate::client_transport::ExecServerReconnectStrategy;
37use crate::connection::JsonRpcConnection;
38use crate::environment::EnvironmentConnectionState;
39use crate::process::ExecProcessEvent;
40use crate::process::ExecProcessEventLog;
41use crate::process::ExecProcessEventReceiver;
42use crate::protocol::CAPABILITY_ROOTS_DISCOVER_METHOD;
43use crate::protocol::CapabilityRootsDiscoverParams;
44use crate::protocol::CapabilityRootsDiscoverResponse;
45use crate::protocol::ENVIRONMENT_INFO_METHOD;
46use crate::protocol::ENVIRONMENT_STATUS_METHOD;
47use crate::protocol::EXEC_CLOSED_METHOD;
48use crate::protocol::EXEC_EXITED_METHOD;
49use crate::protocol::EXEC_METHOD;
50use crate::protocol::EXEC_OUTPUT_DELTA_METHOD;
51use crate::protocol::EXEC_READ_METHOD;
52use crate::protocol::EXEC_SIGNAL_METHOD;
53use crate::protocol::EXEC_TERMINATE_METHOD;
54use crate::protocol::EXEC_WRITE_METHOD;
55use crate::protocol::EnvironmentInfo;
56use crate::protocol::EnvironmentStatus;
57use crate::protocol::ExecClosedNotification;
58use crate::protocol::ExecExitedNotification;
59use crate::protocol::ExecOutputDeltaNotification;
60use crate::protocol::ExecParams;
61use crate::protocol::ExecResponse;
62use crate::protocol::FS_CANONICALIZE_METHOD;
63use crate::protocol::FS_CLOSE_METHOD;
64use crate::protocol::FS_COPY_METHOD;
65use crate::protocol::FS_CREATE_DIRECTORY_METHOD;
66use crate::protocol::FS_GET_METADATA_METHOD;
67use crate::protocol::FS_OPEN_METHOD;
68use crate::protocol::FS_READ_BLOCK_METHOD;
69use crate::protocol::FS_READ_DIRECTORY_METHOD;
70use crate::protocol::FS_READ_FILE_METHOD;
71use crate::protocol::FS_REMOVE_METHOD;
72use crate::protocol::FS_WALK_METHOD;
73use crate::protocol::FS_WRITE_FILE_METHOD;
74use crate::protocol::FsCanonicalizeParams;
75use crate::protocol::FsCanonicalizeResponse;
76use crate::protocol::FsCloseParams;
77use crate::protocol::FsCloseResponse;
78use crate::protocol::FsCopyParams;
79use crate::protocol::FsCopyResponse;
80use crate::protocol::FsCreateDirectoryParams;
81use crate::protocol::FsCreateDirectoryResponse;
82use crate::protocol::FsGetMetadataParams;
83use crate::protocol::FsGetMetadataResponse;
84use crate::protocol::FsOpenParams;
85use crate::protocol::FsOpenResponse;
86use crate::protocol::FsReadBlockParams;
87use crate::protocol::FsReadBlockResponse;
88use crate::protocol::FsReadDirectoryParams;
89use crate::protocol::FsReadDirectoryResponse;
90use crate::protocol::FsReadFileParams;
91use crate::protocol::FsReadFileResponse;
92use crate::protocol::FsRemoveParams;
93use crate::protocol::FsRemoveResponse;
94use crate::protocol::FsWalkParams;
95use crate::protocol::FsWalkResponse;
96use crate::protocol::FsWriteFileParams;
97use crate::protocol::FsWriteFileResponse;
98use crate::protocol::HTTP_REQUEST_BODY_DELTA_METHOD;
99use crate::protocol::INITIALIZE_METHOD;
100use crate::protocol::INITIALIZED_METHOD;
101use crate::protocol::InitializeParams;
102use crate::protocol::InitializeResponse;
103use crate::protocol::ProcessOutputChunk;
104use crate::protocol::ProcessSignal;
105use crate::protocol::ReadParams;
106use crate::protocol::ReadResponse;
107use crate::protocol::SignalParams;
108use crate::protocol::SignalResponse;
109use crate::protocol::TerminateParams;
110use crate::protocol::TerminateResponse;
111use crate::protocol::WriteParams;
112use crate::protocol::WriteResponse;
113use crate::rpc::RpcCallError;
114use crate::rpc::RpcClient;
115
116pub(crate) mod http_client;
117#[path = "client_recovery.rs"]
118mod recovery;
119
120const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
121const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
122const ENVIRONMENT_INFO_TIMEOUT: Duration = Duration::from_secs(30);
123const ENVIRONMENT_STATUS_TIMEOUT: Duration = Duration::from_secs(10);
124const PROCESS_EVENT_CHANNEL_CAPACITY: usize = 256;
125const PROCESS_EVENT_RETAINED_BYTES: usize = 1024 * 1024;
126const MAX_PENDING_PROCESS_EVENTS: usize = 256;
127const MAX_PENDING_PROCESS_EVENT_BYTES: usize = 1024 * 1024;
128
129impl Default for ExecServerClientConnectOptions {
130    fn default() -> Self {
131        Self {
132            client_name: "codex-core".to_string(),
133            initialize_timeout: INITIALIZE_TIMEOUT,
134            resume_session_id: None,
135        }
136    }
137}
138
139impl From<RemoteExecServerConnectArgs> for ExecServerClientConnectOptions {
140    fn from(value: RemoteExecServerConnectArgs) -> Self {
141        Self {
142            client_name: value.client_name,
143            initialize_timeout: value.initialize_timeout,
144            resume_session_id: value.resume_session_id,
145        }
146    }
147}
148
149impl From<StdioExecServerConnectArgs> for ExecServerClientConnectOptions {
150    fn from(value: StdioExecServerConnectArgs) -> Self {
151        Self {
152            client_name: value.client_name,
153            initialize_timeout: value.initialize_timeout,
154            resume_session_id: value.resume_session_id,
155        }
156    }
157}
158
159impl RemoteExecServerConnectArgs {
160    pub fn new(websocket_url: String, client_name: String) -> Self {
161        Self {
162            websocket_url,
163            client_name,
164            connect_timeout: CONNECT_TIMEOUT,
165            initialize_timeout: INITIALIZE_TIMEOUT,
166            resume_session_id: None,
167        }
168    }
169}
170
171pub(crate) struct SessionState {
172    wake_tx: watch::Sender<u64>,
173    events: ExecProcessEventLog,
174    ordered_events: StdMutex<OrderedSessionEvents>,
175    recoverable: AtomicBool,
176    next_write_id: AtomicU64,
177}
178
179#[derive(Default)]
180struct OrderedSessionEvents {
181    last_published_seq: u64,
182    exit_published: bool,
183    closed_published: bool,
184    // Server-side output, exit, and closed notifications are emitted by
185    // different tasks and can reach the client out of order. Keep future events
186    // here until all lower sequence numbers have been published.
187    pending: BTreeMap<u64, ExecProcessEvent>,
188    pending_bytes: usize,
189    failure: Option<String>,
190}
191
192#[derive(Clone)]
193pub(crate) struct Session {
194    client: ExecServerClient,
195    process_id: ProcessId,
196    state: Arc<SessionState>,
197}
198
199struct Inner {
200    connection: StdMutex<ConnectionState>,
201    connection_changed: watch::Sender<()>,
202    // The remote transport delivers one shared notification stream for every
203    // process on the connection. Keep a local process_id -> session registry so
204    // we can turn those connection-global notifications into process wakeups
205    // without making notifications the source of truth for output delivery.
206    sessions: ArcSwap<HashMap<ProcessId, Arc<SessionState>>>,
207    // ArcSwap makes reads cheap on the hot notification path, but writes still
208    // need serialization so concurrent register/remove operations do not
209    // overwrite each other's copy-on-write updates.
210    sessions_write_lock: StdMutex<()>,
211    // Streaming HTTP responses are keyed by a client-generated request id
212    // because they share the same connection-global notification channel as
213    // process output. Keep the routing table local to the client so higher
214    // layers can consume body chunks like a normal byte stream.
215    http_body_streams: ArcSwap<HashMap<String, mpsc::Sender<QueuedHttpBodyDelta>>>,
216    http_body_stream_failures: ArcSwap<HashMap<String, String>>,
217    http_body_streams_write_lock: Mutex<()>,
218    http_body_stream_byte_budget: Arc<Semaphore>,
219    http_body_stream_next_id: AtomicU64,
220    session_id: OnceLock<String>,
221    reconnect_strategy: Option<ExecServerReconnectStrategy>,
222}
223
224struct ConnectionState {
225    status: ConnectionStatus,
226    active_process_starts: usize,
227    environment_connection_state_tx: watch::Sender<EnvironmentConnectionState>,
228}
229
230enum ConnectionStatus {
231    Connected(Arc<RpcClient>),
232    Recovering,
233    Failed(String),
234}
235
236impl ConnectionState {
237    fn set_status(&mut self, status: ConnectionStatus) {
238        self.status = status;
239        self.publish_environment_connection_state();
240    }
241
242    fn publish_environment_connection_state(&self) {
243        let state = match &self.status {
244            ConnectionStatus::Connected(rpc_client) if !rpc_client.is_disconnected() => {
245                EnvironmentConnectionState::Connected
246            }
247            ConnectionStatus::Connected(_)
248            | ConnectionStatus::Recovering
249            | ConnectionStatus::Failed(_) => EnvironmentConnectionState::Disconnected,
250        };
251        let _ = self
252            .environment_connection_state_tx
253            .send_if_modified(|current| {
254                if *current == state {
255                    false
256                } else {
257                    *current = state;
258                    true
259                }
260            });
261    }
262}
263
264#[derive(Clone, Copy)]
265enum RecoveryPolicy {
266    Wait,
267    FailFast,
268}
269
270#[derive(Clone)]
271pub struct ExecServerClient {
272    inner: Arc<Inner>,
273    recovery_policy: RecoveryPolicy,
274}
275
276struct ActiveProcessStart {
277    inner: Arc<Inner>,
278}
279
280impl Drop for ActiveProcessStart {
281    fn drop(&mut self) {
282        self.inner.finish_process_start();
283    }
284}
285
286type ConnectionResult = Result<ExecServerClient, Arc<ExecServerError>>;
287type ConnectionAttempt = OnceCell<ConnectionResult>;
288
289#[derive(Clone)]
290pub(crate) struct LazyRemoteExecServerClient {
291    transport_params: ExecServerTransportParams,
292    recovery_policy: RecoveryPolicy,
293    // Saves the first startup result so callers share it and failures remain final.
294    startup: Arc<ConnectionAttempt>,
295    // The latest successful client, replaced whenever reconnecting succeeds.
296    current_client: Arc<StdMutex<Option<ExecServerClient>>>,
297    reconnect: Arc<StdMutex<Option<Arc<ConnectionAttempt>>>>,
298    environment_connection_state_tx: watch::Sender<EnvironmentConnectionState>,
299}
300
301impl LazyRemoteExecServerClient {
302    pub(crate) fn new(transport_params: ExecServerTransportParams) -> Self {
303        Self {
304            transport_params,
305            recovery_policy: RecoveryPolicy::Wait,
306            startup: Arc::new(ConnectionAttempt::new()),
307            current_client: Arc::new(StdMutex::new(None)),
308            reconnect: Arc::new(StdMutex::new(None)),
309            environment_connection_state_tx: watch::channel(
310                EnvironmentConnectionState::Disconnected,
311            )
312            .0,
313        }
314    }
315
316    pub(crate) fn subscribe_connection_state(&self) -> watch::Receiver<EnvironmentConnectionState> {
317        self.environment_connection_state_tx.subscribe()
318    }
319
320    pub(crate) fn start_connecting(&self) -> Option<AbortOnDropHandle<()>> {
321        // Stdio starts a process, so keep it lazy until the environment is used.
322        if matches!(
323            self.transport_params,
324            ExecServerTransportParams::StdioCommand { .. }
325        ) {
326            return None;
327        }
328        let client = self.clone();
329        Some(AbortOnDropHandle::new(tokio::spawn(async move {
330            if let Err(error) = client.wait_until_ready().await {
331                debug!(%error, "exec-server environment startup failed");
332            }
333        })))
334    }
335
336    pub(crate) fn startup_finished(&self) -> bool {
337        self.startup.get().is_some()
338    }
339
340    pub(crate) fn readiness_result(&self) -> Option<Result<(), ExecServerError>> {
341        if let Some(client) = self.cached_client() {
342            return client.readiness_result();
343        }
344        self.startup.get().and_then(|result| match result {
345            Ok(client) => client.readiness_result(),
346            Err(error) => Some(Err(ExecServerError::ConnectionAttempt(Arc::clone(error)))),
347        })
348    }
349
350    pub(crate) async fn status(&self) -> crate::EnvironmentObservedStatus {
351        // Fail-fast lookup preserves the non-mutating contract: never start or recover a client.
352        let client = match self.fail_fast().get().await {
353            Ok(client) => client,
354            Err(error) => {
355                // Without a completed startup attempt, there is no exec-server connection to probe.
356                if self.cached_client().is_none() && self.startup.get().is_none() {
357                    return crate::EnvironmentObservedStatus::Pending;
358                }
359                // A known connection failure is reported without retrying it as part of status.
360                return crate::EnvironmentObservedStatus::Disconnected {
361                    error: error.to_string(),
362                };
363            }
364        };
365        // Every callable client is probed so callers never receive a cached health result.
366        match client.environment_status().await {
367            Ok(_) => crate::EnvironmentObservedStatus::Ready,
368            Err(error) => crate::EnvironmentObservedStatus::Disconnected {
369                error: error.to_string(),
370            },
371        }
372    }
373
374    pub(crate) fn fail_fast(&self) -> Self {
375        Self {
376            recovery_policy: RecoveryPolicy::FailFast,
377            ..self.clone()
378        }
379    }
380
381    pub(crate) async fn wait_until_ready(&self) -> Result<(), ExecServerError> {
382        self.initial_client().await.map(drop)
383    }
384
385    pub(crate) async fn get(&self) -> Result<ExecServerClient, ExecServerError> {
386        if matches!(self.recovery_policy, RecoveryPolicy::FailFast) {
387            let client = match self.cached_client() {
388                Some(client) => client,
389                None => match self.startup.get() {
390                    Some(Ok(client)) => client.clone(),
391                    Some(Err(error)) => {
392                        return Err(ExecServerError::ConnectionAttempt(Arc::clone(error)));
393                    }
394                    None => {
395                        return Err(ExecServerError::Disconnected(
396                            "exec-server environment is not ready".to_string(),
397                        ));
398                    }
399                },
400            };
401            return client.fail_fast();
402        }
403        if let Some(client) = self.connected_client() {
404            return Ok(client);
405        }
406
407        let Some(cached_client) = self.cached_client() else {
408            let client = self.initial_client().await?;
409            if !client.is_disconnected() || !self.can_reconnect() {
410                return Ok(client);
411            }
412            return self.reconnect().await;
413        };
414
415        if !self.can_reconnect() {
416            return Ok(cached_client);
417        }
418
419        self.reconnect().await
420    }
421
422    async fn initial_client(&self) -> Result<ExecServerClient, ExecServerError> {
423        // The first caller starts the work; every other caller waits for that same result.
424        let result = self.startup.get_or_init(|| self.connect_once()).await;
425        match result {
426            Ok(client) => {
427                let mut current_client = self
428                    .current_client
429                    .lock()
430                    .unwrap_or_else(std::sync::PoisonError::into_inner);
431                if current_client.is_none() {
432                    *current_client = Some(client.clone());
433                }
434                Ok(client.clone())
435            }
436            Err(error) => Err(ExecServerError::ConnectionAttempt(Arc::clone(error))),
437        }
438    }
439
440    async fn reconnect(&self) -> Result<ExecServerClient, ExecServerError> {
441        // Callers handling the same outage share one reconnect attempt.
442        let attempt = {
443            let mut reconnect = self
444                .reconnect
445                .lock()
446                .unwrap_or_else(std::sync::PoisonError::into_inner);
447            if let Some(client) = self.connected_client() {
448                return Ok(client);
449            }
450            reconnect
451                .get_or_insert_with(|| Arc::new(ConnectionAttempt::new()))
452                .clone()
453        };
454        let result = attempt
455            .get_or_init(|| async {
456                let result = self.connect_once().await;
457                if let Ok(client) = &result {
458                    *self
459                        .current_client
460                        .lock()
461                        .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(client.clone());
462                }
463                result
464            })
465            .await;
466        let mut reconnect = self
467            .reconnect
468            .lock()
469            .unwrap_or_else(std::sync::PoisonError::into_inner);
470        // Forget only this completed attempt so a later operation can retry after failure.
471        if reconnect
472            .as_ref()
473            .is_some_and(|current| Arc::ptr_eq(current, &attempt))
474        {
475            *reconnect = None;
476        }
477        result.clone().map_err(ExecServerError::ConnectionAttempt)
478    }
479
480    fn connected_client(&self) -> Option<ExecServerClient> {
481        self.cached_client()
482            .filter(|client| !client.is_disconnected())
483    }
484
485    fn cached_client(&self) -> Option<ExecServerClient> {
486        self.current_client
487            .lock()
488            .unwrap_or_else(std::sync::PoisonError::into_inner)
489            .clone()
490    }
491
492    fn can_reconnect(&self) -> bool {
493        matches!(
494            self.transport_params,
495            ExecServerTransportParams::Deferred(_)
496                | ExecServerTransportParams::WebSocketUrl { .. }
497                | ExecServerTransportParams::NoiseRendezvous { .. }
498        )
499    }
500
501    async fn connect_once(&self) -> ConnectionResult {
502        let result = ExecServerClient::connect_for_transport(self.transport_params.clone())
503            .await
504            .map_err(Arc::new);
505        if let Ok(client) = &result {
506            client
507                .attach_environment_connection_state(self.environment_connection_state_tx.clone());
508        }
509        result
510    }
511}
512
513impl HttpClient for LazyRemoteExecServerClient {
514    fn http_request(
515        &self,
516        params: crate::HttpRequestParams,
517    ) -> BoxFuture<'_, Result<crate::HttpRequestResponse, ExecServerError>> {
518        async move { self.get().await?.http_request(params).await }.boxed()
519    }
520
521    fn http_request_stream(
522        &self,
523        params: crate::HttpRequestParams,
524    ) -> BoxFuture<
525        '_,
526        Result<(crate::HttpRequestResponse, crate::HttpResponseBodyStream), ExecServerError>,
527    > {
528        async move { self.get().await?.http_request_stream(params).await }.boxed()
529    }
530}
531
532impl LazyRemoteExecServerClient {
533    pub(crate) async fn environment_info(&self) -> Result<EnvironmentInfo, ExecServerError> {
534        self.get().await?.environment_info().await
535    }
536}
537
538#[derive(Debug, thiserror::Error)]
539pub enum ExecServerError {
540    #[error("failed to spawn exec-server: {0}")]
541    Spawn(#[source] std::io::Error),
542    #[error("timed out connecting to exec-server websocket `{url}` after {timeout:?}")]
543    WebSocketConnectTimeout { url: String, timeout: Duration },
544    #[error("failed to connect to exec-server websocket `{url}`: {source}")]
545    WebSocketConnect {
546        url: String,
547        #[source]
548        source: tokio_tungstenite::tungstenite::Error,
549    },
550    #[error("timed out waiting for exec-server initialize handshake after {timeout:?}")]
551    InitializeTimedOut { timeout: Duration },
552    #[error("exec-server transport closed")]
553    Closed,
554    #[error("{0}")]
555    Disconnected(String),
556    #[error("failed to serialize or deserialize exec-server JSON: {0}")]
557    Json(#[from] serde_json::Error),
558    #[error("HTTP request failed: {0}")]
559    HttpRequest(String),
560    #[error("exec-server protocol error: {0}")]
561    Protocol(String),
562    #[error("exec-server rejected request ({code}): {message}")]
563    Server { code: i64, message: String },
564    #[error("environment registry request failed ({status}{code_suffix}): {message}", code_suffix = .code.as_ref().map(|code| format!(", {code}")).unwrap_or_default())]
565    EnvironmentRegistryHttp {
566        status: reqwest::StatusCode,
567        code: Option<String>,
568        message: String,
569    },
570    #[error("environment registry configuration error: {0}")]
571    EnvironmentRegistryConfig(String),
572    #[error("environment registry authentication error: {0}")]
573    EnvironmentRegistryAuth(String),
574    #[error("environment registry request failed: {0}")]
575    EnvironmentRegistryRequest(#[from] reqwest::Error),
576    #[error("exec-server connection attempt failed: {0}")]
577    ConnectionAttempt(#[source] Arc<ExecServerError>),
578}
579
580impl ExecServerClient {
581    fn attach_environment_connection_state(
582        &self,
583        state_tx: watch::Sender<EnvironmentConnectionState>,
584    ) {
585        let mut connection = self
586            .inner
587            .connection
588            .lock()
589            .unwrap_or_else(std::sync::PoisonError::into_inner);
590        connection.environment_connection_state_tx = state_tx;
591        connection.publish_environment_connection_state();
592    }
593
594    fn fail_fast(&self) -> Result<Self, ExecServerError> {
595        self.rpc_client_without_recovery()?;
596        Ok(Self {
597            inner: Arc::clone(&self.inner),
598            recovery_policy: RecoveryPolicy::FailFast,
599        })
600    }
601
602    fn rpc_client_without_recovery(&self) -> Result<Arc<RpcClient>, ExecServerError> {
603        let connection = self
604            .inner
605            .connection
606            .lock()
607            .unwrap_or_else(std::sync::PoisonError::into_inner);
608        match &connection.status {
609            ConnectionStatus::Connected(rpc_client) if !rpc_client.is_disconnected() => {
610                Ok(Arc::clone(rpc_client))
611            }
612            ConnectionStatus::Connected(_) | ConnectionStatus::Recovering => Err(
613                ExecServerError::Disconnected("exec-server environment is recovering".to_string()),
614            ),
615            ConnectionStatus::Failed(message) => {
616                Err(ExecServerError::Disconnected(message.clone()))
617            }
618        }
619    }
620
621    async fn rpc_client(&self) -> Result<Arc<RpcClient>, ExecServerError> {
622        match self.recovery_policy {
623            RecoveryPolicy::Wait => self.inner.rpc_client().await,
624            RecoveryPolicy::FailFast => self.rpc_client_without_recovery(),
625        }
626    }
627
628    async fn initialize_rpc(
629        &self,
630        rpc_client: &RpcClient,
631        options: ExecServerClientConnectOptions,
632    ) -> Result<InitializeResponse, ExecServerError> {
633        let ExecServerClientConnectOptions {
634            client_name,
635            initialize_timeout,
636            resume_session_id,
637        } = options;
638
639        timeout(initialize_timeout, async {
640            let response: InitializeResponse = rpc_client
641                .call(
642                    INITIALIZE_METHOD,
643                    &InitializeParams {
644                        client_name,
645                        resume_session_id,
646                    },
647                )
648                .await?;
649            let session_id = self
650                .inner
651                .session_id
652                .get_or_init(|| response.session_id.clone());
653            if session_id != &response.session_id {
654                return Err(ExecServerError::Protocol(format!(
655                    "exec-server initialized an unexpected session {}",
656                    response.session_id
657                )));
658            }
659            rpc_client
660                .notify(INITIALIZED_METHOD, &serde_json::json!({}))
661                .await?;
662            Ok(response)
663        })
664        .await
665        .map_err(|_| ExecServerError::InitializeTimedOut {
666            timeout: initialize_timeout,
667        })?
668    }
669
670    pub async fn exec(&self, params: ExecParams) -> Result<ExecResponse, ExecServerError> {
671        self.call(EXEC_METHOD, &params).await
672    }
673
674    pub async fn environment_info(&self) -> Result<EnvironmentInfo, ExecServerError> {
675        let rpc_client = self.rpc_client().await?;
676        map_rpc_call_result(
677            rpc_client
678                .call_with_timeout(ENVIRONMENT_INFO_METHOD, &(), ENVIRONMENT_INFO_TIMEOUT)
679                .await,
680        )
681    }
682
683    pub async fn environment_status(&self) -> Result<EnvironmentStatus, ExecServerError> {
684        // Health checks only reuse an existing RPC connection and never initiate recovery.
685        let rpc_client = self.rpc_client_without_recovery()?;
686        map_rpc_call_result(
687            rpc_client
688                .call_with_timeout(ENVIRONMENT_STATUS_METHOD, &(), ENVIRONMENT_STATUS_TIMEOUT)
689                .await,
690        )
691    }
692
693    pub async fn discover_capability_roots(
694        &self,
695        params: CapabilityRootsDiscoverParams,
696    ) -> Result<CapabilityRootsDiscoverResponse, ExecServerError> {
697        self.call(CAPABILITY_ROOTS_DISCOVER_METHOD, &params).await
698    }
699
700    pub async fn read(&self, params: ReadParams) -> Result<ReadResponse, ExecServerError> {
701        self.call(EXEC_READ_METHOD, &params).await
702    }
703
704    pub async fn write(
705        &self,
706        process_id: &ProcessId,
707        chunk: Vec<u8>,
708        write_id: String,
709    ) -> Result<WriteResponse, ExecServerError> {
710        self.call(
711            EXEC_WRITE_METHOD,
712            &WriteParams {
713                process_id: process_id.clone(),
714                chunk: chunk.into(),
715                write_id,
716            },
717        )
718        .await
719    }
720
721    pub async fn signal(
722        &self,
723        process_id: &ProcessId,
724        signal: ProcessSignal,
725    ) -> Result<(), ExecServerError> {
726        let _response: SignalResponse = self
727            .call(
728                EXEC_SIGNAL_METHOD,
729                &SignalParams {
730                    process_id: process_id.clone(),
731                    signal,
732                },
733            )
734            .await?;
735        Ok(())
736    }
737
738    pub async fn terminate(
739        &self,
740        process_id: &ProcessId,
741    ) -> Result<TerminateResponse, ExecServerError> {
742        self.call_for_cleanup(
743            EXEC_TERMINATE_METHOD,
744            &TerminateParams {
745                process_id: process_id.clone(),
746            },
747        )
748        .await
749    }
750
751    pub async fn fs_read_file(
752        &self,
753        params: FsReadFileParams,
754    ) -> Result<FsReadFileResponse, ExecServerError> {
755        self.call(FS_READ_FILE_METHOD, &params).await
756    }
757
758    pub async fn fs_open(&self, params: FsOpenParams) -> Result<FsOpenResponse, ExecServerError> {
759        self.call(FS_OPEN_METHOD, &params).await
760    }
761
762    pub async fn fs_read_block(
763        &self,
764        params: FsReadBlockParams,
765    ) -> Result<FsReadBlockResponse, ExecServerError> {
766        self.call(FS_READ_BLOCK_METHOD, &params).await
767    }
768
769    pub async fn fs_close(
770        &self,
771        params: FsCloseParams,
772    ) -> Result<FsCloseResponse, ExecServerError> {
773        self.call_for_cleanup(FS_CLOSE_METHOD, &params).await
774    }
775
776    pub async fn fs_write_file(
777        &self,
778        params: FsWriteFileParams,
779    ) -> Result<FsWriteFileResponse, ExecServerError> {
780        self.call(FS_WRITE_FILE_METHOD, &params).await
781    }
782
783    pub async fn fs_create_directory(
784        &self,
785        params: FsCreateDirectoryParams,
786    ) -> Result<FsCreateDirectoryResponse, ExecServerError> {
787        self.call(FS_CREATE_DIRECTORY_METHOD, &params).await
788    }
789
790    pub async fn fs_get_metadata(
791        &self,
792        params: FsGetMetadataParams,
793    ) -> Result<FsGetMetadataResponse, ExecServerError> {
794        self.call(FS_GET_METADATA_METHOD, &params).await
795    }
796
797    pub async fn fs_canonicalize(
798        &self,
799        params: FsCanonicalizeParams,
800    ) -> Result<FsCanonicalizeResponse, ExecServerError> {
801        self.call(FS_CANONICALIZE_METHOD, &params).await
802    }
803
804    pub async fn fs_read_directory(
805        &self,
806        params: FsReadDirectoryParams,
807    ) -> Result<FsReadDirectoryResponse, ExecServerError> {
808        self.call(FS_READ_DIRECTORY_METHOD, &params).await
809    }
810
811    pub async fn fs_walk(&self, params: FsWalkParams) -> Result<FsWalkResponse, ExecServerError> {
812        self.call(FS_WALK_METHOD, &params).await
813    }
814
815    pub async fn fs_remove(
816        &self,
817        params: FsRemoveParams,
818    ) -> Result<FsRemoveResponse, ExecServerError> {
819        self.call(FS_REMOVE_METHOD, &params).await
820    }
821
822    pub async fn fs_copy(&self, params: FsCopyParams) -> Result<FsCopyResponse, ExecServerError> {
823        self.call(FS_COPY_METHOD, &params).await
824    }
825
826    pub(crate) async fn start_process(
827        &self,
828        params: ExecParams,
829    ) -> Result<Session, ExecServerError> {
830        loop {
831            let rpc_client = self.rpc_client().await?;
832            if !self.inner.begin_process_start(&rpc_client) {
833                continue;
834            }
835
836            let process_id = params.process_id.clone();
837            let state = Arc::new(SessionState::new(/*recoverable*/ false));
838            if let Err(error) = self.inner.insert_session(&process_id, Arc::clone(&state)) {
839                self.inner.finish_process_start();
840                return Err(error);
841            }
842            let active_start = ActiveProcessStart {
843                inner: Arc::clone(&self.inner),
844            };
845            let client = self.clone();
846            let (result_tx, result_rx) = tokio::sync::oneshot::channel();
847            let process_start_task = async move {
848                let _active_start = active_start;
849                match client
850                    .call_rpc::<_, ExecResponse>(&rpc_client, EXEC_METHOD, &params)
851                    .await
852                {
853                    Ok(_) => {
854                        state.recoverable.store(true, Ordering::Release);
855                        let session = Session {
856                            client: client.clone(),
857                            process_id: process_id.clone(),
858                            state: Arc::clone(&state),
859                        };
860                        if result_tx.send(Ok(session)).is_err() {
861                            state.recoverable.store(false, Ordering::Release);
862                            tokio::spawn(async move {
863                                cleanup_process_start(&client, &process_id, &state).await;
864                            });
865                        }
866                    }
867                    Err(error) => {
868                        if is_transport_closed_error(&error) {
869                            tokio::spawn(async move {
870                                cleanup_process_start(&client, &process_id, &state).await;
871                            });
872                        } else {
873                            client.inner.remove_session_if(&process_id, &state);
874                        }
875                        let _ = result_tx.send(Err(error));
876                    }
877                }
878            };
879            tokio::spawn(
880                process_start_task
881                    .in_current_span()
882                    .with_current_subscriber(),
883            );
884            return result_rx.await.map_err(|_| {
885                ExecServerError::Protocol("process start task stopped unexpectedly".to_string())
886            })?;
887        }
888    }
889
890    #[cfg(test)]
891    pub(crate) async fn register_session(
892        &self,
893        process_id: &ProcessId,
894    ) -> Result<Session, ExecServerError> {
895        let state = Arc::new(SessionState::new(/*recoverable*/ true));
896        self.inner.insert_session(process_id, Arc::clone(&state))?;
897        Ok(Session {
898            client: self.clone(),
899            process_id: process_id.clone(),
900            state,
901        })
902    }
903
904    pub fn session_id(&self) -> Option<String> {
905        self.inner.session_id.get().cloned()
906    }
907
908    fn is_disconnected(&self) -> bool {
909        self.inner.is_failed()
910    }
911
912    fn readiness_result(&self) -> Option<Result<(), ExecServerError>> {
913        let connection = self
914            .inner
915            .connection
916            .lock()
917            .unwrap_or_else(std::sync::PoisonError::into_inner);
918        match &connection.status {
919            ConnectionStatus::Connected(rpc_client) if !rpc_client.is_disconnected() => {
920                Some(Ok(()))
921            }
922            ConnectionStatus::Connected(_) | ConnectionStatus::Recovering => None,
923            ConnectionStatus::Failed(message) => {
924                Some(Err(ExecServerError::Disconnected(message.clone())))
925            }
926        }
927    }
928
929    pub(crate) async fn connect(
930        connection: JsonRpcConnection,
931        options: ExecServerClientConnectOptions,
932    ) -> Result<Self, ExecServerError> {
933        Self::connect_with_recovery(connection, options, /*reconnect_strategy*/ None).await
934    }
935
936    pub(crate) async fn connect_with_recovery(
937        connection: JsonRpcConnection,
938        options: ExecServerClientConnectOptions,
939        reconnect_strategy: Option<ExecServerReconnectStrategy>,
940    ) -> Result<Self, ExecServerError> {
941        let (rpc_client, events_rx) = RpcClient::new(connection);
942        let rpc_client = Arc::new(rpc_client);
943        let session_id = OnceLock::new();
944        let (connection_changed, _connection_changed_rx) = watch::channel(());
945        let inner = Arc::new(Inner {
946            connection: StdMutex::new(ConnectionState {
947                status: ConnectionStatus::Connected(Arc::clone(&rpc_client)),
948                active_process_starts: 0,
949                environment_connection_state_tx: watch::channel(
950                    EnvironmentConnectionState::Connected,
951                )
952                .0,
953            }),
954            connection_changed,
955            sessions: ArcSwap::from_pointee(HashMap::new()),
956            sessions_write_lock: StdMutex::new(()),
957            http_body_streams: ArcSwap::from_pointee(HashMap::new()),
958            http_body_stream_failures: ArcSwap::from_pointee(HashMap::new()),
959            http_body_streams_write_lock: Mutex::new(()),
960            http_body_stream_byte_budget: Arc::new(Semaphore::new(MAX_QUEUED_HTTP_BODY_BYTES)),
961            http_body_stream_next_id: AtomicU64::new(1),
962            session_id,
963            reconnect_strategy,
964        });
965        let client = Self {
966            inner,
967            recovery_policy: RecoveryPolicy::Wait,
968        };
969        // An explicit resume can redirect notifications from running processes
970        // before initialize returns. Drain them immediately so a burst cannot
971        // fill the bounded event channel and block the initialize response.
972        client.spawn_rpc_reader(&rpc_client, events_rx);
973        client.initialize_rpc(&rpc_client, options).await?;
974        Ok(client)
975    }
976
977    async fn call<P, T>(&self, method: &str, params: &P) -> Result<T, ExecServerError>
978    where
979        P: serde::Serialize,
980        T: serde::de::DeserializeOwned,
981    {
982        let rpc_client = self.rpc_client().await?;
983        self.call_rpc(&rpc_client, method, params).await
984    }
985
986    async fn call_rpc<P, T>(
987        &self,
988        rpc_client: &Arc<RpcClient>,
989        method: &str,
990        params: &P,
991    ) -> Result<T, ExecServerError>
992    where
993        P: serde::Serialize,
994        T: serde::de::DeserializeOwned,
995    {
996        map_rpc_call_result(rpc_client.call(method, params).await)
997    }
998
999    async fn call_for_cleanup<P, T>(&self, method: &str, params: &P) -> Result<T, ExecServerError>
1000    where
1001        P: serde::Serialize,
1002        T: serde::de::DeserializeOwned,
1003    {
1004        let rpc_client = self.inner.rpc_client().await?;
1005        map_rpc_call_result(rpc_client.call_for_cleanup(method, params).await)
1006    }
1007}
1008
1009fn map_rpc_call_result<T>(result: Result<T, RpcCallError>) -> Result<T, ExecServerError> {
1010    result.map_err(|error| {
1011        let error = ExecServerError::from(error);
1012        if is_transport_closed_error(&error) {
1013            ExecServerError::Disconnected(disconnected_message(/*reason*/ None))
1014        } else {
1015            error
1016        }
1017    })
1018}
1019
1020async fn cleanup_process_start(
1021    client: &ExecServerClient,
1022    process_id: &ProcessId,
1023    state: &Arc<SessionState>,
1024) {
1025    loop {
1026        match client.terminate(process_id).await {
1027            Ok(_) => break,
1028            Err(error) if is_transport_closed_error(&error) && !client.inner.is_failed() => {
1029                continue;
1030            }
1031            Err(_) => break,
1032        }
1033    }
1034    client.inner.remove_session_if(process_id, state);
1035}
1036
1037impl From<RpcCallError> for ExecServerError {
1038    fn from(value: RpcCallError) -> Self {
1039        match value {
1040            RpcCallError::Closed => Self::Closed,
1041            RpcCallError::Json(err) => Self::Json(err),
1042            RpcCallError::Server(error) => Self::Server {
1043                code: error.code,
1044                message: error.message,
1045            },
1046            RpcCallError::TimedOut { method, timeout } => Self::Protocol(format!(
1047                "timed out waiting for exec-server `{method}` response after {timeout:?}"
1048            )),
1049            RpcCallError::PendingRequestLimitExceeded { limit } => Self::Protocol(format!(
1050                "exec-server has reached its limit of {limit} pending requests"
1051            )),
1052        }
1053    }
1054}
1055
1056impl SessionState {
1057    fn new(recoverable: bool) -> Self {
1058        let (wake_tx, _wake_rx) = watch::channel(0);
1059        Self {
1060            wake_tx,
1061            events: ExecProcessEventLog::new(
1062                PROCESS_EVENT_CHANNEL_CAPACITY,
1063                PROCESS_EVENT_RETAINED_BYTES,
1064            ),
1065            ordered_events: StdMutex::new(OrderedSessionEvents::default()),
1066            recoverable: AtomicBool::new(recoverable),
1067            next_write_id: AtomicU64::new(1),
1068        }
1069    }
1070
1071    pub(crate) fn subscribe(&self) -> watch::Receiver<u64> {
1072        self.wake_tx.subscribe()
1073    }
1074
1075    pub(crate) fn subscribe_events(&self) -> ExecProcessEventReceiver {
1076        self.events.subscribe()
1077    }
1078
1079    fn note_change(&self, seq: u64) {
1080        self.wake_tx
1081            .send_modify(|current| *current = (*current).max(seq));
1082    }
1083
1084    /// Publishes a process event only when all earlier sequenced events have
1085    /// already been published.
1086    ///
1087    /// Returns `true` only when this call actually publishes the ordered
1088    /// `Closed` event. The caller uses that signal to remove the session route
1089    /// after the terminal event is visible to subscribers, rather than when a
1090    /// possibly-early closed notification first arrives.
1091    fn publish_ordered_event(&self, event: ExecProcessEvent) -> Result<bool, String> {
1092        let Some(seq) = event.seq() else {
1093            self.events.publish(event);
1094            return Ok(false);
1095        };
1096
1097        let mut ordered_events = self
1098            .ordered_events
1099            .lock()
1100            .unwrap_or_else(std::sync::PoisonError::into_inner);
1101        // We have already delivered this sequence number or moved past it,
1102        // so accepting it again would duplicate output or lifecycle events.
1103        if ordered_events.failure.is_some()
1104            || ordered_events.closed_published
1105            || seq <= ordered_events.last_published_seq
1106        {
1107            return Ok(false);
1108        }
1109
1110        ordered_events.insert_pending(event)?;
1111        Ok(self.publish_ready(&mut ordered_events))
1112    }
1113
1114    fn publish_ready(&self, ordered_events: &mut OrderedSessionEvents) -> bool {
1115        let mut published_closed = false;
1116        loop {
1117            let next_seq = ordered_events.last_published_seq.saturating_add(1);
1118            let Some(event) = ordered_events.pending.remove(&next_seq) else {
1119                break;
1120            };
1121            ordered_events.pending_bytes = ordered_events
1122                .pending_bytes
1123                .saturating_sub(pending_process_event_bytes(&event));
1124            ordered_events.last_published_seq = next_seq;
1125            ordered_events.exit_published |= matches!(&event, ExecProcessEvent::Exited { .. });
1126            let is_closed = matches!(&event, ExecProcessEvent::Closed { .. });
1127            ordered_events.closed_published |= is_closed;
1128            published_closed |= is_closed;
1129            self.events.publish(event);
1130        }
1131        published_closed
1132    }
1133
1134    fn set_failure(&self, message: String) {
1135        let mut ordered_events = self
1136            .ordered_events
1137            .lock()
1138            .unwrap_or_else(std::sync::PoisonError::into_inner);
1139        if ordered_events.failure.is_some() || ordered_events.closed_published {
1140            return;
1141        }
1142        ordered_events.failure = Some(message.clone());
1143        ordered_events.pending.clear();
1144        ordered_events.pending_bytes = 0;
1145        self.events.publish(ExecProcessEvent::Failed(message));
1146        drop(ordered_events);
1147        self.wake_tx
1148            .send_modify(|current| *current = current.saturating_add(1));
1149    }
1150
1151    fn failed_response(&self) -> Option<ReadResponse> {
1152        self.ordered_events
1153            .lock()
1154            .unwrap_or_else(std::sync::PoisonError::into_inner)
1155            .failure
1156            .clone()
1157            .map(|message| self.synthesized_failure(message))
1158    }
1159
1160    fn synthesized_failure(&self, message: String) -> ReadResponse {
1161        let next_seq = (*self.wake_tx.borrow()).saturating_add(1);
1162        ReadResponse {
1163            chunks: Vec::new(),
1164            next_seq,
1165            exited: true,
1166            exit_code: None,
1167            closed: true,
1168            failure: Some(message),
1169            sandbox_denied: false,
1170        }
1171    }
1172
1173    fn next_write_id(&self) -> String {
1174        self.next_write_id
1175            .fetch_add(1, Ordering::Relaxed)
1176            .to_string()
1177    }
1178}
1179
1180impl OrderedSessionEvents {
1181    fn insert_pending(&mut self, event: ExecProcessEvent) -> Result<(), String> {
1182        let Some(seq) = event.seq() else {
1183            return Err("cannot reorder an unsequenced process event".to_string());
1184        };
1185        if self.pending.contains_key(&seq) {
1186            return Ok(());
1187        }
1188
1189        let next_seq = self.last_published_seq.saturating_add(1);
1190        // The next expected event is synchronously published by every caller,
1191        // so it can drain a full buffer without becoming retained state.
1192        let closes_gap = seq == next_seq;
1193        if !closes_gap && self.pending.len() >= MAX_PENDING_PROCESS_EVENTS {
1194            return Err(format!(
1195                "process event reorder buffer exceeds {MAX_PENDING_PROCESS_EVENTS} entries"
1196            ));
1197        }
1198
1199        let event_bytes = pending_process_event_bytes(&event);
1200        if event_bytes > MAX_PENDING_PROCESS_EVENT_BYTES {
1201            return Err(format!(
1202                "process event exceeds {MAX_PENDING_PROCESS_EVENT_BYTES} bytes"
1203            ));
1204        }
1205        let pending_bytes = self.pending_bytes.saturating_add(event_bytes);
1206        if !closes_gap && pending_bytes > MAX_PENDING_PROCESS_EVENT_BYTES {
1207            return Err(format!(
1208                "process event reorder buffer exceeds {MAX_PENDING_PROCESS_EVENT_BYTES} bytes"
1209            ));
1210        }
1211
1212        self.pending.insert(seq, event);
1213        self.pending_bytes = pending_bytes;
1214        Ok(())
1215    }
1216}
1217
1218fn pending_process_event_bytes(event: &ExecProcessEvent) -> usize {
1219    match event {
1220        ExecProcessEvent::Output(chunk) => chunk.chunk.0.len(),
1221        ExecProcessEvent::Failed(message) => message.len(),
1222        ExecProcessEvent::Exited { .. } | ExecProcessEvent::Closed { .. } => 0,
1223    }
1224}
1225
1226fn finish_process_event(
1227    inner: &Inner,
1228    process_id: &ProcessId,
1229    session: &Arc<SessionState>,
1230    result: Result<bool, String>,
1231) {
1232    match result {
1233        Ok(true) => inner.remove_session_if(process_id, session),
1234        Ok(false) => {}
1235        Err(message) => {
1236            session.set_failure(message);
1237            inner.remove_session_if(process_id, session);
1238        }
1239    }
1240}
1241
1242impl Session {
1243    pub(crate) fn process_id(&self) -> &ProcessId {
1244        &self.process_id
1245    }
1246
1247    pub(crate) fn subscribe_wake(&self) -> watch::Receiver<u64> {
1248        self.state.subscribe()
1249    }
1250
1251    pub(crate) fn subscribe_events(&self) -> ExecProcessEventReceiver {
1252        self.state.subscribe_events()
1253    }
1254
1255    pub(crate) async fn read(
1256        &self,
1257        after_seq: Option<u64>,
1258        max_bytes: Option<usize>,
1259        wait_ms: Option<u64>,
1260    ) -> Result<ReadResponse, ExecServerError> {
1261        loop {
1262            if let Some(response) = self.state.failed_response() {
1263                return Ok(response);
1264            }
1265
1266            match self
1267                .client
1268                .read(ReadParams {
1269                    process_id: self.process_id.clone(),
1270                    after_seq,
1271                    max_bytes,
1272                    wait_ms,
1273                })
1274                .await
1275            {
1276                Ok(response) => return Ok(response),
1277                Err(error)
1278                    if is_transport_closed_error(&error) && !self.client.inner.is_failed() =>
1279                {
1280                    continue;
1281                }
1282                Err(error) if is_transport_closed_error(&error) => {
1283                    if let Some(response) = self.state.failed_response() {
1284                        return Ok(response);
1285                    }
1286                    let message = error.to_string();
1287                    self.state.set_failure(message.clone());
1288                    return Ok(self.state.synthesized_failure(message));
1289                }
1290                Err(error) => return Err(error),
1291            }
1292        }
1293    }
1294
1295    pub(crate) async fn write(&self, chunk: Vec<u8>) -> Result<WriteResponse, ExecServerError> {
1296        let write_id = self.state.next_write_id();
1297        loop {
1298            match self
1299                .client
1300                .write(&self.process_id, chunk.clone(), write_id.clone())
1301                .await
1302            {
1303                Ok(response) => return Ok(response),
1304                Err(error)
1305                    if is_transport_closed_error(&error) && !self.client.inner.is_failed() =>
1306                {
1307                    continue;
1308                }
1309                Err(error) => return Err(error),
1310            }
1311        }
1312    }
1313
1314    pub(crate) async fn signal(&self, signal: ProcessSignal) -> Result<(), ExecServerError> {
1315        self.client.signal(&self.process_id, signal).await
1316    }
1317
1318    pub(crate) async fn terminate(&self) -> Result<(), ExecServerError> {
1319        self.client.terminate(&self.process_id).await?;
1320        Ok(())
1321    }
1322
1323    pub(crate) async fn unregister(&self) {
1324        self.client
1325            .inner
1326            .remove_session_if(&self.process_id, &self.state);
1327    }
1328}
1329
1330impl Inner {
1331    fn get_session(&self, process_id: &ProcessId) -> Option<Arc<SessionState>> {
1332        self.sessions.load().get(process_id).cloned()
1333    }
1334
1335    fn insert_session(
1336        &self,
1337        process_id: &ProcessId,
1338        session: Arc<SessionState>,
1339    ) -> Result<(), ExecServerError> {
1340        let _sessions_write_guard = self
1341            .sessions_write_lock
1342            .lock()
1343            .unwrap_or_else(std::sync::PoisonError::into_inner);
1344        // Do not register a process session that can never receive environment
1345        // notifications. Without this check, remote MCP startup could create a
1346        // dead session and wait for process output that will never arrive.
1347        if let Some(message) = self.failure_message() {
1348            return Err(ExecServerError::Disconnected(message));
1349        }
1350        let sessions = self.sessions.load();
1351        if sessions.contains_key(process_id) {
1352            return Err(ExecServerError::Protocol(format!(
1353                "session already registered for process {process_id}"
1354            )));
1355        }
1356        let mut next_sessions = sessions.as_ref().clone();
1357        next_sessions.insert(process_id.clone(), session);
1358        self.sessions.store(Arc::new(next_sessions));
1359        Ok(())
1360    }
1361
1362    fn remove_session_if(&self, process_id: &ProcessId, expected: &Arc<SessionState>) {
1363        let _sessions_write_guard = self
1364            .sessions_write_lock
1365            .lock()
1366            .unwrap_or_else(std::sync::PoisonError::into_inner);
1367        let sessions = self.sessions.load();
1368        if !sessions
1369            .get(process_id)
1370            .is_some_and(|session| Arc::ptr_eq(session, expected))
1371        {
1372            return;
1373        }
1374        let mut next_sessions = sessions.as_ref().clone();
1375        next_sessions.remove(process_id);
1376        self.sessions.store(Arc::new(next_sessions));
1377    }
1378
1379    fn take_all_sessions(&self) -> HashMap<ProcessId, Arc<SessionState>> {
1380        let _sessions_write_guard = self
1381            .sessions_write_lock
1382            .lock()
1383            .unwrap_or_else(std::sync::PoisonError::into_inner);
1384        let sessions = self.sessions.load();
1385        let drained_sessions = sessions.as_ref().clone();
1386        self.sessions.store(Arc::new(HashMap::new()));
1387        drained_sessions
1388    }
1389}
1390
1391fn disconnected_message(reason: Option<&str>) -> String {
1392    match reason {
1393        Some(reason) => format!("exec-server transport disconnected: {reason}"),
1394        None => "exec-server transport disconnected".to_string(),
1395    }
1396}
1397
1398fn is_transport_closed_error(error: &ExecServerError) -> bool {
1399    matches!(
1400        error,
1401        ExecServerError::Closed | ExecServerError::Disconnected(_)
1402    ) || matches!(
1403        error,
1404        ExecServerError::Server {
1405            code: -32000,
1406            message,
1407        } if message == "JSON-RPC transport closed"
1408    )
1409}
1410
1411fn fail_all_sessions(inner: &Arc<Inner>, message: String) {
1412    let sessions = inner.take_all_sessions();
1413
1414    for (_, session) in sessions {
1415        // Sessions synthesize a closed read response and emit a pushed Failed
1416        // event. That covers both polling consumers and streaming consumers
1417        // such as environment-backed MCP stdio.
1418        session.set_failure(message.clone());
1419    }
1420}
1421
1422/// Fails all in-flight work that depends on the shared JSON-RPC transport.
1423async fn fail_all_in_flight_work(inner: &Arc<Inner>, message: String) {
1424    fail_all_sessions(inner, message.clone());
1425    inner.fail_all_http_body_streams(message).await;
1426}
1427
1428async fn handle_server_notification(
1429    inner: &Arc<Inner>,
1430    notification: JSONRPCNotification,
1431) -> Result<(), ExecServerError> {
1432    match notification.method.as_str() {
1433        EXEC_OUTPUT_DELTA_METHOD => {
1434            let params: ExecOutputDeltaNotification =
1435                serde_json::from_value(notification.params.unwrap_or(Value::Null))?;
1436            if let Some(session) = inner.get_session(&params.process_id) {
1437                let result =
1438                    session.publish_ordered_event(ExecProcessEvent::Output(ProcessOutputChunk {
1439                        seq: params.seq,
1440                        stream: params.stream,
1441                        chunk: params.chunk,
1442                    }));
1443                if result.is_ok() {
1444                    session.note_change(params.seq);
1445                }
1446                finish_process_event(inner, &params.process_id, &session, result);
1447            }
1448        }
1449        EXEC_EXITED_METHOD => {
1450            let params: ExecExitedNotification =
1451                serde_json::from_value(notification.params.unwrap_or(Value::Null))?;
1452            if let Some(session) = inner.get_session(&params.process_id) {
1453                let result = session.publish_ordered_event(ExecProcessEvent::Exited {
1454                    seq: params.seq,
1455                    exit_code: params.exit_code,
1456                    sandbox_denied: params.sandbox_denied,
1457                });
1458                if result.is_ok() {
1459                    session.note_change(params.seq);
1460                }
1461                finish_process_event(inner, &params.process_id, &session, result);
1462            }
1463        }
1464        EXEC_CLOSED_METHOD => {
1465            let params: ExecClosedNotification =
1466                serde_json::from_value(notification.params.unwrap_or(Value::Null))?;
1467            if let Some(session) = inner.get_session(&params.process_id) {
1468                // Closed is terminal, but it can arrive before tail output or
1469                // exited. Keep routing this process until the ordered publisher
1470                // says Closed has actually been delivered.
1471                let result =
1472                    session.publish_ordered_event(ExecProcessEvent::Closed { seq: params.seq });
1473                if result.is_ok() {
1474                    session.note_change(params.seq);
1475                }
1476                finish_process_event(inner, &params.process_id, &session, result);
1477            }
1478        }
1479        HTTP_REQUEST_BODY_DELTA_METHOD => {
1480            inner
1481                .handle_http_body_delta_notification(notification.params)
1482                .await?;
1483        }
1484        other => {
1485            debug!("ignoring unknown exec-server notification: {other}");
1486        }
1487    }
1488    Ok(())
1489}
1490
1491#[cfg(test)]
1492mod tests {
1493    use codex_exec_server_protocol::JSONRPCMessage;
1494    use codex_exec_server_protocol::JSONRPCNotification;
1495    use codex_exec_server_protocol::JSONRPCResponse;
1496    use codex_utils_path_uri::PathUri;
1497    use futures::SinkExt;
1498    use futures::StreamExt;
1499    use opentelemetry::trace::TracerProvider as _;
1500    use opentelemetry_sdk::trace::SdkTracerProvider;
1501    use pretty_assertions::assert_eq;
1502    use std::collections::HashMap;
1503    #[cfg(unix)]
1504    use std::path::Path;
1505    #[cfg(unix)]
1506    use std::process::Command;
1507    use std::sync::Arc;
1508    use tokio::io::AsyncBufReadExt;
1509    use tokio::io::AsyncWrite;
1510    use tokio::io::AsyncWriteExt;
1511    use tokio::io::BufReader;
1512    use tokio::io::duplex;
1513    use tokio::net::TcpListener;
1514    use tokio::net::TcpStream;
1515    use tokio::sync::mpsc;
1516    use tokio::sync::oneshot;
1517    use tokio::sync::watch;
1518    use tokio::time::Duration;
1519    #[cfg(unix)]
1520    use tokio::time::sleep;
1521    use tokio::time::timeout;
1522    use tokio_tungstenite::WebSocketStream;
1523    use tokio_tungstenite::accept_async;
1524    use tokio_tungstenite::tungstenite::Message;
1525    use tracing::Instrument;
1526    use tracing_subscriber::filter::filter_fn;
1527    use tracing_subscriber::prelude::*;
1528
1529    use super::ExecServerClient;
1530    use super::ExecServerClientConnectOptions;
1531    use super::LazyRemoteExecServerClient;
1532    use crate::EnvironmentObservedStatus;
1533    use crate::ProcessId;
1534    #[cfg(not(windows))]
1535    use crate::client_api::DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT;
1536    use crate::client_api::ExecServerTransportParams;
1537    use crate::client_api::RemoteExecServerConnectArgs;
1538    use crate::client_api::StdioExecServerCommand;
1539    use crate::client_api::StdioExecServerConnectArgs;
1540    use crate::connection::JsonRpcConnection;
1541    use crate::process::ExecProcessEvent;
1542    use crate::protocol::EXEC_CLOSED_METHOD;
1543    use crate::protocol::EXEC_EXITED_METHOD;
1544    use crate::protocol::EXEC_METHOD;
1545    use crate::protocol::EXEC_OUTPUT_DELTA_METHOD;
1546    use crate::protocol::EXEC_READ_METHOD;
1547    use crate::protocol::EXEC_WRITE_METHOD;
1548    use crate::protocol::ExecClosedNotification;
1549    use crate::protocol::ExecExitedNotification;
1550    use crate::protocol::ExecOutputDeltaNotification;
1551    use crate::protocol::ExecOutputStream;
1552    use crate::protocol::ExecParams;
1553    use crate::protocol::ExecResponse;
1554    use crate::protocol::INITIALIZE_METHOD;
1555    use crate::protocol::INITIALIZED_METHOD;
1556    use crate::protocol::InitializeResponse;
1557    use crate::protocol::ProcessOutputChunk;
1558    use crate::protocol::ReadResponse;
1559    use crate::protocol::WriteParams;
1560    use crate::protocol::WriteResponse;
1561    use crate::protocol::WriteStatus;
1562
1563    async fn read_jsonrpc_line<R>(lines: &mut tokio::io::Lines<BufReader<R>>) -> JSONRPCMessage
1564    where
1565        R: tokio::io::AsyncRead + Unpin,
1566    {
1567        let line = timeout(Duration::from_secs(1), lines.next_line())
1568            .await
1569            .expect("json-rpc read should not time out")
1570            .expect("json-rpc read should succeed")
1571            .expect("json-rpc connection should stay open");
1572        serde_json::from_str(&line).expect("json-rpc line should parse")
1573    }
1574
1575    async fn write_jsonrpc_line<W>(writer: &mut W, message: JSONRPCMessage)
1576    where
1577        W: AsyncWrite + Unpin,
1578    {
1579        let encoded = serde_json::to_string(&message).expect("json-rpc message should serialize");
1580        writer
1581            .write_all(format!("{encoded}\n").as_bytes())
1582            .await
1583            .expect("json-rpc line should write");
1584    }
1585
1586    #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
1587    async fn process_start_propagates_caller_trace_context_across_background_task() {
1588        let (client_stdin, server_reader) = duplex(1 << 20);
1589        let (mut server_writer, client_stdout) = duplex(1 << 20);
1590        let server = tokio::spawn(async move {
1591            let mut lines = BufReader::new(server_reader).lines();
1592            let initialize = read_jsonrpc_line(&mut lines).await;
1593            let initialize = match initialize {
1594                JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
1595                other => panic!("expected initialize request, got {other:?}"),
1596            };
1597            write_jsonrpc_line(
1598                &mut server_writer,
1599                JSONRPCMessage::Response(JSONRPCResponse {
1600                    id: initialize.id,
1601                    result: serde_json::to_value(InitializeResponse {
1602                        session_id: "trace-test".to_string(),
1603                    })
1604                    .expect("initialize response should serialize"),
1605                }),
1606            )
1607            .await;
1608
1609            match read_jsonrpc_line(&mut lines).await {
1610                JSONRPCMessage::Notification(notification)
1611                    if notification.method == INITIALIZED_METHOD => {}
1612                other => panic!("expected initialized notification, got {other:?}"),
1613            }
1614
1615            let request = match read_jsonrpc_line(&mut lines).await {
1616                JSONRPCMessage::Request(request) if request.method == EXEC_METHOD => request,
1617                other => panic!("expected process start request, got {other:?}"),
1618            };
1619            let trace = request.trace.clone();
1620            let params: ExecParams =
1621                serde_json::from_value(request.params.expect("process start params should exist"))
1622                    .expect("process start params should deserialize");
1623            write_jsonrpc_line(
1624                &mut server_writer,
1625                JSONRPCMessage::Response(JSONRPCResponse {
1626                    id: request.id,
1627                    result: serde_json::to_value(ExecResponse {
1628                        process_id: params.process_id,
1629                    })
1630                    .expect("process start response should serialize"),
1631                }),
1632            )
1633            .await;
1634            trace
1635        });
1636
1637        let client = ExecServerClient::connect(
1638            JsonRpcConnection::from_stdio(
1639                client_stdout,
1640                client_stdin,
1641                "trace-test-client".to_string(),
1642            ),
1643            ExecServerClientConnectOptions::default(),
1644        )
1645        .await
1646        .expect("client should connect");
1647
1648        let tracer_provider = SdkTracerProvider::builder().build();
1649        let tracer = tracer_provider.tracer("exec-server-test");
1650        let subscriber = tracing_subscriber::registry().with(
1651            tracing_opentelemetry::layer()
1652                .with_tracer(tracer)
1653                .with_filter(filter_fn(codex_otel::OtelProvider::trace_export_filter)),
1654        );
1655        let _subscriber_guard = tracing::subscriber::set_default(subscriber);
1656        tracing::callsite::rebuild_interest_cache();
1657        let parent_span = tracing::info_span!("process-start-parent");
1658        let expected_trace = codex_otel::span_w3c_trace_context(&parent_span)
1659            .expect("parent span should have trace context");
1660        let process_id = ProcessId::from("trace-process");
1661
1662        let session = client
1663            .start_process(ExecParams {
1664                process_id: process_id.clone(),
1665                argv: vec!["true".to_string()],
1666                cwd: PathUri::from_host_native_path(std::env::current_dir().expect("cwd"))
1667                    .expect("cwd URI"),
1668                env_policy: None,
1669                env: HashMap::new(),
1670                tty: false,
1671                pipe_stdin: false,
1672                arg0: None,
1673                sandbox: None,
1674                enforce_managed_network: false,
1675                managed_network: None,
1676                network_proxy: None,
1677            })
1678            .instrument(parent_span)
1679            .await
1680            .expect("process start should succeed");
1681
1682        assert_eq!(session.process_id(), &process_id);
1683        let trace = server.await.expect("server task").expect("trace context");
1684        let expected_traceparent = expected_trace
1685            .traceparent
1686            .as_deref()
1687            .expect("parent traceparent");
1688        let traceparent = trace.traceparent.as_deref().expect("request traceparent");
1689        let expected_parts = expected_traceparent.split('-').collect::<Vec<_>>();
1690        let parts = traceparent.split('-').collect::<Vec<_>>();
1691        assert_eq!(parts[1], expected_parts[1]);
1692        assert_ne!(parts[2], expected_parts[2]);
1693        assert_eq!(trace.tracestate, expected_trace.tracestate);
1694    }
1695
1696    async fn accept_websocket(listener: &TcpListener) -> WebSocketStream<TcpStream> {
1697        let (stream, _) = listener.accept().await.expect("listener should accept");
1698        accept_async(stream)
1699            .await
1700            .expect("websocket handshake should succeed")
1701    }
1702
1703    async fn read_jsonrpc_websocket(websocket: &mut WebSocketStream<TcpStream>) -> JSONRPCMessage {
1704        loop {
1705            match timeout(Duration::from_secs(1), websocket.next())
1706                .await
1707                .expect("json-rpc websocket read should not time out")
1708                .expect("websocket should stay open")
1709                .expect("websocket frame should read")
1710            {
1711                Message::Text(text) => {
1712                    return serde_json::from_str(text.as_ref())
1713                        .expect("json-rpc text frame should parse");
1714                }
1715                Message::Binary(bytes) => {
1716                    return serde_json::from_slice(bytes.as_ref())
1717                        .expect("json-rpc binary frame should parse");
1718                }
1719                Message::Ping(_) | Message::Pong(_) => {}
1720                other => panic!("expected json-rpc websocket frame, got {other:?}"),
1721            }
1722        }
1723    }
1724
1725    async fn write_jsonrpc_websocket(
1726        websocket: &mut WebSocketStream<TcpStream>,
1727        message: JSONRPCMessage,
1728    ) {
1729        let encoded = serde_json::to_string(&message).expect("json-rpc should serialize");
1730        websocket
1731            .send(Message::Text(encoded.into()))
1732            .await
1733            .expect("json-rpc websocket frame should write");
1734    }
1735
1736    async fn complete_websocket_initialize(
1737        websocket: &mut WebSocketStream<TcpStream>,
1738        session_id: &str,
1739        expected_resume_session_id: Option<&str>,
1740    ) {
1741        let initialize = read_jsonrpc_websocket(websocket).await;
1742        let request = match initialize {
1743            JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
1744            other => panic!("expected initialize request, got {other:?}"),
1745        };
1746        let params: crate::protocol::InitializeParams =
1747            serde_json::from_value(request.params.expect("initialize params should exist"))
1748                .expect("initialize params should deserialize");
1749        assert_eq!(
1750            params.resume_session_id.as_deref(),
1751            expected_resume_session_id
1752        );
1753        write_jsonrpc_websocket(
1754            websocket,
1755            JSONRPCMessage::Response(JSONRPCResponse {
1756                id: request.id,
1757                result: serde_json::to_value(InitializeResponse {
1758                    session_id: session_id.to_string(),
1759                })
1760                .expect("initialize response should serialize"),
1761            }),
1762        )
1763        .await;
1764
1765        let initialized = read_jsonrpc_websocket(websocket).await;
1766        match initialized {
1767            JSONRPCMessage::Notification(notification)
1768                if notification.method == INITIALIZED_METHOD => {}
1769            other => panic!("expected initialized notification, got {other:?}"),
1770        }
1771    }
1772
1773    #[cfg(not(windows))]
1774    #[tokio::test]
1775    async fn connect_stdio_command_initializes_json_rpc_client() {
1776        let client = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1777            command: StdioExecServerCommand {
1778                program: "sh".to_string(),
1779                args: vec![
1780                    "-c".to_string(),
1781                    "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\"}}'; read _line; sleep 60".to_string(),
1782                ],
1783                env: HashMap::new(),
1784                cwd: None,
1785            },
1786            client_name: "stdio-test-client".to_string(),
1787            initialize_timeout: Duration::from_secs(1),
1788            resume_session_id: None,
1789        })
1790        .await
1791        .expect("stdio client should connect");
1792
1793        assert_eq!(client.session_id().as_deref(), Some("stdio-test"));
1794    }
1795
1796    #[cfg(not(windows))]
1797    #[tokio::test]
1798    async fn connect_for_transport_initializes_stdio_command() {
1799        let client = ExecServerClient::connect_for_transport(
1800            ExecServerTransportParams::StdioCommand {
1801                command: StdioExecServerCommand {
1802                    program: "sh".to_string(),
1803                    args: vec![
1804                        "-c".to_string(),
1805                        "read _line; printf '%s\\n' '{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\"}}'; read _line; sleep 60".to_string(),
1806                    ],
1807                    env: HashMap::new(),
1808                    cwd: None,
1809                },
1810                initialize_timeout: DEFAULT_REMOTE_EXEC_SERVER_INITIALIZE_TIMEOUT,
1811            },
1812        )
1813        .await
1814        .expect("stdio transport should connect");
1815
1816        assert_eq!(client.session_id().as_deref(), Some("stdio-test"));
1817    }
1818
1819    #[cfg(windows)]
1820    #[tokio::test]
1821    async fn connect_stdio_command_initializes_json_rpc_client_on_windows() {
1822        let client = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1823            command: StdioExecServerCommand {
1824                program: "powershell".to_string(),
1825                args: vec![
1826                    "-NoProfile".to_string(),
1827                    "-Command".to_string(),
1828                    "$null = [Console]::In.ReadLine(); [Console]::Out.WriteLine('{\"id\":1,\"result\":{\"sessionId\":\"stdio-test\"}}'); $null = [Console]::In.ReadLine(); Start-Sleep -Seconds 60".to_string(),
1829                ],
1830                env: HashMap::new(),
1831                cwd: None,
1832            },
1833            client_name: "stdio-test-client".to_string(),
1834            initialize_timeout: Duration::from_secs(1),
1835            resume_session_id: None,
1836        })
1837        .await
1838        .expect("stdio client should connect");
1839
1840        assert_eq!(client.session_id().as_deref(), Some("stdio-test"));
1841    }
1842
1843    #[cfg(unix)]
1844    #[tokio::test]
1845    async fn dropping_stdio_client_terminates_spawned_process() {
1846        let tempdir = tempfile::tempdir().expect("tempdir should be created");
1847        let pid_file = tempdir.path().join("server.pid");
1848        let child_pid_file = tempdir.path().join("server-child.pid");
1849        let stdio_script = format!(
1850            "read _line; \
1851             echo \"$$\" > {}; \
1852             sleep 60 >/dev/null 2>&1 & echo \"$!\" > {}; \
1853             printf '%s\\n' '{{\"id\":1,\"result\":{{\"sessionId\":\"stdio-test\"}}}}'; \
1854             read _line; \
1855             wait",
1856            shell_quote(pid_file.as_path()),
1857            shell_quote(child_pid_file.as_path()),
1858        );
1859
1860        let client = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1861            command: StdioExecServerCommand {
1862                program: "sh".to_string(),
1863                args: vec!["-c".to_string(), stdio_script],
1864                env: HashMap::new(),
1865                cwd: None,
1866            },
1867            client_name: "stdio-test-client".to_string(),
1868            initialize_timeout: Duration::from_secs(1),
1869            resume_session_id: None,
1870        })
1871        .await
1872        .expect("stdio client should connect");
1873        let server_pid = read_pid_file(pid_file.as_path()).await;
1874        let child_pid = read_pid_file(child_pid_file.as_path()).await;
1875        assert!(
1876            process_exists(server_pid),
1877            "spawned stdio process should be running before client drop"
1878        );
1879        assert!(
1880            process_exists(child_pid),
1881            "spawned stdio child process should be running before client drop"
1882        );
1883
1884        drop(client);
1885
1886        wait_for_process_exit(server_pid).await;
1887        wait_for_process_exit(child_pid).await;
1888    }
1889
1890    #[cfg(unix)]
1891    #[tokio::test]
1892    async fn malformed_stdio_message_terminates_spawned_process() {
1893        let tempdir = tempfile::tempdir().expect("tempdir should be created");
1894        let pid_file = tempdir.path().join("server.pid");
1895        let stdio_script = format!(
1896            "read _line; \
1897             echo \"$$\" > {}; \
1898             printf '%s\\n' 'not-json'; \
1899             sleep 60",
1900            shell_quote(pid_file.as_path()),
1901        );
1902
1903        let result = ExecServerClient::connect_stdio_command(StdioExecServerConnectArgs {
1904            command: StdioExecServerCommand {
1905                program: "sh".to_string(),
1906                args: vec!["-c".to_string(), stdio_script],
1907                env: HashMap::new(),
1908                cwd: None,
1909            },
1910            client_name: "stdio-test-client".to_string(),
1911            initialize_timeout: Duration::from_secs(1),
1912            resume_session_id: None,
1913        })
1914        .await;
1915        assert!(result.is_err(), "malformed stdio server should not connect");
1916
1917        let server_pid = read_pid_file(pid_file.as_path()).await;
1918        wait_for_process_exit(server_pid).await;
1919    }
1920
1921    #[cfg(unix)]
1922    async fn read_pid_file(path: &Path) -> u32 {
1923        for _ in 0..20 {
1924            if let Ok(contents) = std::fs::read_to_string(path) {
1925                return contents
1926                    .trim()
1927                    .parse()
1928                    .expect("pid file should contain a pid");
1929            }
1930            sleep(Duration::from_millis(50)).await;
1931        }
1932        panic!("pid file {} should be written", path.display());
1933    }
1934
1935    #[cfg(unix)]
1936    async fn wait_for_process_exit(pid: u32) {
1937        for _ in 0..20 {
1938            if !process_exists(pid) {
1939                return;
1940            }
1941            sleep(Duration::from_millis(100)).await;
1942        }
1943        panic!("process {pid} should exit");
1944    }
1945
1946    #[cfg(unix)]
1947    fn process_exists(pid: u32) -> bool {
1948        Command::new("kill")
1949            .arg("-0")
1950            .arg(pid.to_string())
1951            .status()
1952            .is_ok_and(|status| status.success())
1953    }
1954
1955    #[cfg(unix)]
1956    fn shell_quote(path: &Path) -> String {
1957        let value = path.to_string_lossy();
1958        format!("'{}'", value.replace('\'', "'\\''"))
1959    }
1960
1961    #[tokio::test]
1962    async fn process_events_are_delivered_in_seq_order_when_notifications_are_reordered() {
1963        let (client_stdin, server_reader) = duplex(1 << 20);
1964        let (mut server_writer, client_stdout) = duplex(1 << 20);
1965        let (notifications_tx, mut notifications_rx) = mpsc::channel(16);
1966        let server = tokio::spawn(async move {
1967            let mut lines = BufReader::new(server_reader).lines();
1968            let initialize = read_jsonrpc_line(&mut lines).await;
1969            let request = match initialize {
1970                JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
1971                other => panic!("expected initialize request, got {other:?}"),
1972            };
1973            write_jsonrpc_line(
1974                &mut server_writer,
1975                JSONRPCMessage::Response(JSONRPCResponse {
1976                    id: request.id,
1977                    result: serde_json::to_value(InitializeResponse {
1978                        session_id: "session-1".to_string(),
1979                    })
1980                    .expect("initialize response should serialize"),
1981                }),
1982            )
1983            .await;
1984
1985            let initialized = read_jsonrpc_line(&mut lines).await;
1986            match initialized {
1987                JSONRPCMessage::Notification(notification)
1988                    if notification.method == INITIALIZED_METHOD => {}
1989                other => panic!("expected initialized notification, got {other:?}"),
1990            }
1991
1992            while let Some(message) = notifications_rx.recv().await {
1993                write_jsonrpc_line(&mut server_writer, message).await;
1994            }
1995        });
1996
1997        let client = ExecServerClient::connect(
1998            JsonRpcConnection::from_stdio(
1999                client_stdout,
2000                client_stdin,
2001                "test-exec-server-client".to_string(),
2002            ),
2003            ExecServerClientConnectOptions::default(),
2004        )
2005        .await
2006        .expect("client should connect");
2007
2008        let process_id = ProcessId::from("reordered");
2009        let session = client
2010            .register_session(&process_id)
2011            .await
2012            .expect("session should register");
2013        let mut events = session.subscribe_events();
2014
2015        for message in [
2016            JSONRPCMessage::Notification(JSONRPCNotification {
2017                method: EXEC_CLOSED_METHOD.to_string(),
2018                params: Some(
2019                    serde_json::to_value(ExecClosedNotification {
2020                        process_id: process_id.clone(),
2021                        seq: 4,
2022                    })
2023                    .expect("closed notification should serialize"),
2024                ),
2025            }),
2026            JSONRPCMessage::Notification(JSONRPCNotification {
2027                method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2028                params: Some(
2029                    serde_json::to_value(ExecOutputDeltaNotification {
2030                        process_id: process_id.clone(),
2031                        seq: 1,
2032                        stream: ExecOutputStream::Stdout,
2033                        chunk: b"one".to_vec().into(),
2034                    })
2035                    .expect("output notification should serialize"),
2036                ),
2037            }),
2038            JSONRPCMessage::Notification(JSONRPCNotification {
2039                method: EXEC_EXITED_METHOD.to_string(),
2040                params: Some(
2041                    serde_json::to_value(ExecExitedNotification {
2042                        process_id: process_id.clone(),
2043                        seq: 3,
2044                        exit_code: 0,
2045                        sandbox_denied: Some(true),
2046                    })
2047                    .expect("exit notification should serialize"),
2048                ),
2049            }),
2050            JSONRPCMessage::Notification(JSONRPCNotification {
2051                method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2052                params: Some(
2053                    serde_json::to_value(ExecOutputDeltaNotification {
2054                        process_id: process_id.clone(),
2055                        seq: 2,
2056                        stream: ExecOutputStream::Stderr,
2057                        chunk: b"two".to_vec().into(),
2058                    })
2059                    .expect("output notification should serialize"),
2060                ),
2061            }),
2062        ] {
2063            notifications_tx
2064                .send(message)
2065                .await
2066                .expect("notification should queue");
2067        }
2068
2069        let mut delivered = Vec::new();
2070        for _ in 0..4 {
2071            delivered.push(
2072                timeout(Duration::from_secs(1), events.recv())
2073                    .await
2074                    .expect("process event should not time out")
2075                    .expect("process event stream should stay open"),
2076            );
2077        }
2078
2079        assert_eq!(
2080            delivered,
2081            vec![
2082                ExecProcessEvent::Output(ProcessOutputChunk {
2083                    seq: 1,
2084                    stream: ExecOutputStream::Stdout,
2085                    chunk: b"one".to_vec().into(),
2086                }),
2087                ExecProcessEvent::Output(ProcessOutputChunk {
2088                    seq: 2,
2089                    stream: ExecOutputStream::Stderr,
2090                    chunk: b"two".to_vec().into(),
2091                }),
2092                ExecProcessEvent::Exited {
2093                    seq: 3,
2094                    exit_code: 0,
2095                    sandbox_denied: Some(true),
2096                },
2097                ExecProcessEvent::Closed { seq: 4 },
2098            ]
2099        );
2100
2101        drop(notifications_tx);
2102        drop(client);
2103        server.await.expect("server task should finish");
2104    }
2105
2106    #[tokio::test]
2107    async fn transport_disconnect_fails_sessions_and_rejects_new_sessions() {
2108        let (client_stdin, server_reader) = duplex(1 << 20);
2109        let (mut server_writer, client_stdout) = duplex(1 << 20);
2110        let (disconnect_tx, disconnect_rx) = oneshot::channel();
2111        let server = tokio::spawn(async move {
2112            let mut lines = BufReader::new(server_reader).lines();
2113            let initialize = read_jsonrpc_line(&mut lines).await;
2114            let request = match initialize {
2115                JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
2116                other => panic!("expected initialize request, got {other:?}"),
2117            };
2118            write_jsonrpc_line(
2119                &mut server_writer,
2120                JSONRPCMessage::Response(JSONRPCResponse {
2121                    id: request.id,
2122                    result: serde_json::to_value(InitializeResponse {
2123                        session_id: "session-1".to_string(),
2124                    })
2125                    .expect("initialize response should serialize"),
2126                }),
2127            )
2128            .await;
2129
2130            let initialized = read_jsonrpc_line(&mut lines).await;
2131            match initialized {
2132                JSONRPCMessage::Notification(notification)
2133                    if notification.method == INITIALIZED_METHOD => {}
2134                other => panic!("expected initialized notification, got {other:?}"),
2135            }
2136
2137            let _ = disconnect_rx.await;
2138            drop(server_writer);
2139        });
2140
2141        let client = ExecServerClient::connect(
2142            JsonRpcConnection::from_stdio(
2143                client_stdout,
2144                client_stdin,
2145                "test-exec-server-client".to_string(),
2146            ),
2147            ExecServerClientConnectOptions::default(),
2148        )
2149        .await
2150        .expect("client should connect");
2151
2152        let process_id = ProcessId::from("disconnect");
2153        let session = client
2154            .register_session(&process_id)
2155            .await
2156            .expect("session should register");
2157        let mut events = session.subscribe_events();
2158
2159        disconnect_tx.send(()).expect("disconnect should signal");
2160
2161        let event = timeout(Duration::from_secs(1), events.recv())
2162            .await
2163            .expect("session failure should not time out")
2164            .expect("session event stream should stay open");
2165        let ExecProcessEvent::Failed(message) = event else {
2166            panic!("expected session failure after disconnect, got {event:?}");
2167        };
2168        assert_eq!(message, "exec-server transport disconnected");
2169
2170        let response = session
2171            .read(
2172                /*after_seq*/ None, /*max_bytes*/ None, /*wait_ms*/ None,
2173            )
2174            .await
2175            .expect("disconnected session read should synthesize a response");
2176        assert_eq!(
2177            response.failure.as_deref(),
2178            Some("exec-server transport disconnected")
2179        );
2180        assert!(response.closed);
2181
2182        let new_session = client.register_session(&ProcessId::from("new")).await;
2183        assert!(matches!(
2184            new_session,
2185            Err(super::ExecServerError::Disconnected(_))
2186        ));
2187
2188        drop(client);
2189        server.await.expect("server task should finish");
2190    }
2191
2192    #[tokio::test]
2193    async fn remote_websocket_client_resumes_session() {
2194        let listener = TcpListener::bind("127.0.0.1:0")
2195            .await
2196            .expect("listener should bind");
2197        let websocket_url = format!(
2198            "ws://{}",
2199            listener.local_addr().expect("listener should have address")
2200        );
2201        let (resumed_tx, resumed_rx) = oneshot::channel();
2202        let (finish_tx, finish_rx) = oneshot::channel();
2203        let server = tokio::spawn(async move {
2204            let mut first = accept_websocket(&listener).await;
2205            complete_websocket_initialize(
2206                &mut first,
2207                "session-1",
2208                /*expected_resume_session_id*/ None,
2209            )
2210            .await;
2211            first.close(None).await.expect("websocket should close");
2212
2213            let mut resumed = accept_websocket(&listener).await;
2214            complete_websocket_initialize(
2215                &mut resumed,
2216                "session-1",
2217                /*expected_resume_session_id*/ Some("session-1"),
2218            )
2219            .await;
2220            resumed_tx.send(()).expect("resume should signal");
2221            finish_rx.await.expect("test should finish");
2222        });
2223
2224        let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2225            websocket_url,
2226            connect_timeout: Duration::from_secs(1),
2227            initialize_timeout: Duration::from_secs(1),
2228        });
2229        let stable_client = client.get().await.expect("client should connect");
2230        timeout(Duration::from_secs(1), resumed_rx)
2231            .await
2232            .expect("session resume should not time out")
2233            .expect("session resume should signal");
2234        let reused_client = client.get().await.expect("client should stay connected");
2235        assert_eq!(stable_client.session_id().as_deref(), Some("session-1"));
2236        assert!(Arc::ptr_eq(&stable_client.inner, &reused_client.inner));
2237        finish_tx.send(()).expect("test should finish");
2238        server.await.expect("server task should finish");
2239    }
2240
2241    #[tokio::test]
2242    async fn session_write_retries_same_write_id_after_recovery() {
2243        let listener = TcpListener::bind("127.0.0.1:0")
2244            .await
2245            .expect("listener should bind");
2246        let websocket_url = format!(
2247            "ws://{}",
2248            listener.local_addr().expect("listener should have address")
2249        );
2250        let (finish_tx, finish_rx) = oneshot::channel();
2251        let server = tokio::spawn(async move {
2252            let mut first = accept_websocket(&listener).await;
2253            complete_websocket_initialize(
2254                &mut first,
2255                "session-1",
2256                /*expected_resume_session_id*/ None,
2257            )
2258            .await;
2259
2260            let first_write = read_jsonrpc_websocket(&mut first).await;
2261            let first_write = match first_write {
2262                JSONRPCMessage::Request(request) if request.method == EXEC_WRITE_METHOD => request,
2263                other => panic!("expected first process/write request, got {other:?}"),
2264            };
2265            let first_write_params: WriteParams =
2266                serde_json::from_value(first_write.params.expect("write params should exist"))
2267                    .expect("write params should deserialize");
2268            assert_eq!(first_write_params.process_id.as_str(), "proc-write");
2269            assert_eq!(first_write_params.chunk.into_inner(), b"hello\n".to_vec());
2270            let write_id = first_write_params.write_id;
2271            assert!(!write_id.is_empty());
2272            drop(first);
2273
2274            let mut resumed = accept_websocket(&listener).await;
2275            complete_websocket_initialize(
2276                &mut resumed,
2277                "session-1",
2278                /*expected_resume_session_id*/ Some("session-1"),
2279            )
2280            .await;
2281
2282            let recovery_read = read_jsonrpc_websocket(&mut resumed).await;
2283            let recovery_read = match recovery_read {
2284                JSONRPCMessage::Request(request) if request.method == EXEC_READ_METHOD => request,
2285                other => panic!("expected recovery process/read request, got {other:?}"),
2286            };
2287            write_jsonrpc_websocket(
2288                &mut resumed,
2289                JSONRPCMessage::Response(JSONRPCResponse {
2290                    id: recovery_read.id,
2291                    result: serde_json::to_value(ReadResponse {
2292                        chunks: Vec::new(),
2293                        next_seq: 1,
2294                        exited: false,
2295                        exit_code: None,
2296                        closed: false,
2297                        failure: None,
2298                        sandbox_denied: false,
2299                    })
2300                    .expect("read response should serialize"),
2301                }),
2302            )
2303            .await;
2304
2305            let retried_write = read_jsonrpc_websocket(&mut resumed).await;
2306            let retried_write = match retried_write {
2307                JSONRPCMessage::Request(request) if request.method == EXEC_WRITE_METHOD => request,
2308                other => panic!("expected retried process/write request, got {other:?}"),
2309            };
2310            let retried_write_params: WriteParams =
2311                serde_json::from_value(retried_write.params.expect("write params should exist"))
2312                    .expect("write params should deserialize");
2313            assert_eq!(retried_write_params.process_id.as_str(), "proc-write");
2314            assert_eq!(retried_write_params.chunk.into_inner(), b"hello\n".to_vec());
2315            assert_eq!(retried_write_params.write_id, write_id);
2316            write_jsonrpc_websocket(
2317                &mut resumed,
2318                JSONRPCMessage::Response(JSONRPCResponse {
2319                    id: retried_write.id,
2320                    result: serde_json::to_value(WriteResponse {
2321                        status: WriteStatus::Accepted,
2322                    })
2323                    .expect("write response should serialize"),
2324                }),
2325            )
2326            .await;
2327
2328            finish_rx.await.expect("test should finish");
2329        });
2330
2331        let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2332            websocket_url,
2333            connect_timeout: Duration::from_secs(1),
2334            initialize_timeout: Duration::from_secs(1),
2335        });
2336        let stable_client = client.get().await.expect("client should connect");
2337        let session = stable_client
2338            .register_session(&ProcessId::from("proc-write"))
2339            .await
2340            .expect("session should register");
2341
2342        let response = timeout(Duration::from_secs(2), session.write(b"hello\n".to_vec()))
2343            .await
2344            .expect("write should not time out")
2345            .expect("write should recover");
2346        assert_eq!(
2347            response,
2348            WriteResponse {
2349                status: WriteStatus::Accepted
2350            }
2351        );
2352
2353        finish_tx.send(()).expect("test should finish");
2354        server.await.expect("server task should finish");
2355    }
2356
2357    #[tokio::test]
2358    async fn explicit_resume_drains_notifications_before_initialize_response() {
2359        let listener = TcpListener::bind("127.0.0.1:0")
2360            .await
2361            .expect("listener should bind");
2362        let websocket_url = format!(
2363            "ws://{}",
2364            listener.local_addr().expect("listener should have address")
2365        );
2366        let (initialized_tx, initialized_rx) = oneshot::channel();
2367        let (finish_tx, finish_rx) = oneshot::channel();
2368        let server = tokio::spawn(async move {
2369            let mut websocket = accept_websocket(&listener).await;
2370            let initialize = read_jsonrpc_websocket(&mut websocket).await;
2371            let request = match initialize {
2372                JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
2373                other => panic!("expected initialize request, got {other:?}"),
2374            };
2375            let params: crate::protocol::InitializeParams =
2376                serde_json::from_value(request.params.expect("initialize params should exist"))
2377                    .expect("initialize params should deserialize");
2378            assert_eq!(params.resume_session_id.as_deref(), Some("session-1"));
2379
2380            for seq in 1..=256 {
2381                write_jsonrpc_websocket(
2382                    &mut websocket,
2383                    JSONRPCMessage::Notification(JSONRPCNotification {
2384                        method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2385                        params: Some(
2386                            serde_json::to_value(ExecOutputDeltaNotification {
2387                                process_id: ProcessId::from("busy-process"),
2388                                seq,
2389                                stream: ExecOutputStream::Stdout,
2390                                chunk: b"output".to_vec().into(),
2391                            })
2392                            .expect("output notification should serialize"),
2393                        ),
2394                    }),
2395                )
2396                .await;
2397            }
2398            write_jsonrpc_websocket(
2399                &mut websocket,
2400                JSONRPCMessage::Response(JSONRPCResponse {
2401                    id: request.id,
2402                    result: serde_json::to_value(InitializeResponse {
2403                        session_id: "session-1".to_string(),
2404                    })
2405                    .expect("initialize response should serialize"),
2406                }),
2407            )
2408            .await;
2409
2410            let initialized = read_jsonrpc_websocket(&mut websocket).await;
2411            match initialized {
2412                JSONRPCMessage::Notification(notification)
2413                    if notification.method == INITIALIZED_METHOD => {}
2414                other => panic!("expected initialized notification, got {other:?}"),
2415            }
2416            initialized_tx
2417                .send(())
2418                .expect("initialized notification should signal");
2419            finish_rx.await.expect("test should finish");
2420        });
2421
2422        let client = timeout(
2423            Duration::from_secs(1),
2424            ExecServerClient::connect_websocket(RemoteExecServerConnectArgs {
2425                websocket_url,
2426                client_name: "test-client".to_string(),
2427                connect_timeout: Duration::from_secs(1),
2428                initialize_timeout: Duration::from_secs(1),
2429                resume_session_id: Some("session-1".to_string()),
2430            }),
2431        )
2432        .await
2433        .expect("explicit resume should not time out")
2434        .expect("explicit resume should connect");
2435        assert_eq!(client.session_id().as_deref(), Some("session-1"));
2436
2437        timeout(Duration::from_secs(1), initialized_rx)
2438            .await
2439            .expect("initialized notification should not time out")
2440            .expect("initialized notification should signal");
2441        finish_tx.send(()).expect("test should finish");
2442        server.await.expect("server task should finish");
2443    }
2444
2445    #[tokio::test]
2446    async fn initial_connection_is_shared_by_all_waiters() {
2447        let listener = TcpListener::bind("127.0.0.1:0")
2448            .await
2449            .expect("listener should bind");
2450        let websocket_url = format!(
2451            "ws://{}",
2452            listener.local_addr().expect("listener should have address")
2453        );
2454        let server = tokio::spawn(async move {
2455            let mut connection = accept_websocket(&listener).await;
2456            complete_websocket_initialize(
2457                &mut connection,
2458                "startup-session",
2459                /*expected_resume_session_id*/ None,
2460            )
2461            .await;
2462            timeout(Duration::from_secs(1), connection.next())
2463                .await
2464                .expect("client should close after the test");
2465        });
2466        let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2467            websocket_url,
2468            connect_timeout: Duration::from_secs(1),
2469            initialize_timeout: Duration::from_secs(1),
2470        });
2471
2472        assert!(!client.startup_finished());
2473        let _startup_task = client.start_connecting();
2474        let (ready, first, second) =
2475            tokio::join!(client.wait_until_ready(), client.get(), client.get());
2476        ready.expect("background startup should finish");
2477        let first = first.expect("first waiter should receive the client");
2478        let second = second.expect("second waiter should receive the same client");
2479
2480        assert!(client.startup_finished());
2481        assert_eq!(first.session_id().as_deref(), Some("startup-session"));
2482        assert!(Arc::ptr_eq(&first.inner, &second.inner));
2483
2484        drop(first);
2485        drop(second);
2486        drop(client);
2487        server.await.expect("server task should finish");
2488    }
2489
2490    #[tokio::test]
2491    async fn terminal_stdio_startup_failure_is_remembered() {
2492        let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::StdioCommand {
2493            command: StdioExecServerCommand {
2494                program: "codex-missing-exec-server-for-test".to_string(),
2495                args: Vec::new(),
2496                env: HashMap::new(),
2497                cwd: None,
2498            },
2499            initialize_timeout: Duration::from_secs(1),
2500        });
2501
2502        assert!(client.start_connecting().is_none());
2503        assert!(!client.startup_finished());
2504        let first = match client.get().await {
2505            Ok(_) => panic!("missing executable should fail"),
2506            Err(error) => error,
2507        };
2508        assert!(client.startup_finished());
2509        let second = match client.get().await {
2510            Ok(_) => panic!("burned environment should stay failed"),
2511            Err(error) => error,
2512        };
2513        assert!(matches!(
2514            client.status().await,
2515            EnvironmentObservedStatus::Disconnected { .. }
2516        ));
2517
2518        let (
2519            super::ExecServerError::ConnectionAttempt(first),
2520            super::ExecServerError::ConnectionAttempt(second),
2521        ) = (first, second)
2522        else {
2523            panic!("expected saved connection failures");
2524        };
2525        assert!(Arc::ptr_eq(&first, &second));
2526    }
2527
2528    #[tokio::test]
2529    async fn failed_reconnect_does_not_burn_environment() {
2530        let listener = TcpListener::bind("127.0.0.1:0")
2531            .await
2532            .expect("listener should bind");
2533        let websocket_url = format!(
2534            "ws://{}",
2535            listener.local_addr().expect("listener should have address")
2536        );
2537        let (replacement_initialized_tx, replacement_initialized_rx) = oneshot::channel();
2538        let (allow_replacement_tx, allow_replacement_rx) = watch::channel(false);
2539        let server = tokio::spawn(async move {
2540            let mut first = accept_websocket(&listener).await;
2541            complete_websocket_initialize(
2542                &mut first,
2543                "startup-session",
2544                /*expected_resume_session_id*/ None,
2545            )
2546            .await;
2547            first
2548                .close(None)
2549                .await
2550                .expect("startup websocket should close");
2551
2552            let successful_reconnect = loop {
2553                let (stream, _) = listener.accept().await.expect("reconnect should arrive");
2554                if *allow_replacement_rx.borrow() {
2555                    break stream;
2556                }
2557                let mut failed_reconnect = stream;
2558                failed_reconnect
2559                    .write_all(b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 0\r\n\r\n")
2560                    .await
2561                    .expect("failed handshake response should write");
2562            };
2563            let mut successful_reconnect = accept_async(successful_reconnect)
2564                .await
2565                .expect("replacement websocket handshake should succeed");
2566            complete_websocket_initialize(
2567                &mut successful_reconnect,
2568                "replacement-session",
2569                /*expected_resume_session_id*/ None,
2570            )
2571            .await;
2572            replacement_initialized_tx
2573                .send(())
2574                .expect("replacement initialization should be observed");
2575            timeout(Duration::from_secs(1), successful_reconnect.next())
2576                .await
2577                .expect("client should close after the test");
2578        });
2579        let client = LazyRemoteExecServerClient::new(ExecServerTransportParams::WebSocketUrl {
2580            websocket_url,
2581            connect_timeout: Duration::from_secs(1),
2582            initialize_timeout: Duration::from_secs(1),
2583        });
2584
2585        let initial = client.get().await.expect("startup should connect");
2586        timeout(Duration::from_secs(1), async {
2587            while !initial.is_disconnected() {
2588                tokio::task::yield_now().await;
2589            }
2590        })
2591        .await
2592        .expect("client should observe disconnect");
2593        let failed_reconnect = match client.get().await {
2594            Ok(_) => panic!("first lazy reconnect should fail"),
2595            Err(error) => error,
2596        };
2597        assert!(matches!(
2598            failed_reconnect,
2599            super::ExecServerError::ConnectionAttempt(_)
2600        ));
2601        allow_replacement_tx
2602            .send(true)
2603            .expect("server should allow a fresh client");
2604        let replacement = client.get().await.expect("later reconnect should succeed");
2605
2606        assert_eq!(
2607            replacement.session_id().as_deref(),
2608            Some("replacement-session")
2609        );
2610        replacement_initialized_rx
2611            .await
2612            .expect("server should observe replacement initialization");
2613
2614        drop(initial);
2615        drop(replacement);
2616        drop(client);
2617        server.await.expect("server task should finish");
2618    }
2619
2620    #[tokio::test]
2621    async fn wake_notifications_do_not_block_other_sessions() {
2622        let (client_stdin, server_reader) = duplex(1 << 20);
2623        let (mut server_writer, client_stdout) = duplex(1 << 20);
2624        let (notifications_tx, mut notifications_rx) = mpsc::channel(16);
2625        let server = tokio::spawn(async move {
2626            let mut lines = BufReader::new(server_reader).lines();
2627            let initialize = read_jsonrpc_line(&mut lines).await;
2628            let request = match initialize {
2629                JSONRPCMessage::Request(request) if request.method == INITIALIZE_METHOD => request,
2630                other => panic!("expected initialize request, got {other:?}"),
2631            };
2632            write_jsonrpc_line(
2633                &mut server_writer,
2634                JSONRPCMessage::Response(JSONRPCResponse {
2635                    id: request.id,
2636                    result: serde_json::to_value(InitializeResponse {
2637                        session_id: "session-1".to_string(),
2638                    })
2639                    .expect("initialize response should serialize"),
2640                }),
2641            )
2642            .await;
2643
2644            let initialized = read_jsonrpc_line(&mut lines).await;
2645            match initialized {
2646                JSONRPCMessage::Notification(notification)
2647                    if notification.method == INITIALIZED_METHOD => {}
2648                other => panic!("expected initialized notification, got {other:?}"),
2649            }
2650
2651            while let Some(message) = notifications_rx.recv().await {
2652                write_jsonrpc_line(&mut server_writer, message).await;
2653            }
2654        });
2655
2656        let client = ExecServerClient::connect(
2657            JsonRpcConnection::from_stdio(
2658                client_stdout,
2659                client_stdin,
2660                "test-exec-server-client".to_string(),
2661            ),
2662            ExecServerClientConnectOptions::default(),
2663        )
2664        .await
2665        .expect("client should connect");
2666
2667        let noisy_process_id = ProcessId::from("noisy");
2668        let quiet_process_id = ProcessId::from("quiet");
2669        let _noisy_session = client
2670            .register_session(&noisy_process_id)
2671            .await
2672            .expect("noisy session should register");
2673        let quiet_session = client
2674            .register_session(&quiet_process_id)
2675            .await
2676            .expect("quiet session should register");
2677        let mut quiet_wake_rx = quiet_session.subscribe_wake();
2678
2679        for seq in 0..=4096 {
2680            notifications_tx
2681                .send(JSONRPCMessage::Notification(JSONRPCNotification {
2682                    method: EXEC_OUTPUT_DELTA_METHOD.to_string(),
2683                    params: Some(
2684                        serde_json::to_value(ExecOutputDeltaNotification {
2685                            process_id: noisy_process_id.clone(),
2686                            seq,
2687                            stream: ExecOutputStream::Stdout,
2688                            chunk: b"x".to_vec().into(),
2689                        })
2690                        .expect("output notification should serialize"),
2691                    ),
2692                }))
2693                .await
2694                .expect("output notification should queue");
2695        }
2696
2697        notifications_tx
2698            .send(JSONRPCMessage::Notification(JSONRPCNotification {
2699                method: EXEC_EXITED_METHOD.to_string(),
2700                params: Some(
2701                    serde_json::to_value(ExecExitedNotification {
2702                        process_id: quiet_process_id,
2703                        seq: 1,
2704                        exit_code: 17,
2705                        sandbox_denied: Some(false),
2706                    })
2707                    .expect("exit notification should serialize"),
2708                ),
2709            }))
2710            .await
2711            .expect("exit notification should queue");
2712
2713        timeout(Duration::from_secs(1), quiet_wake_rx.changed())
2714            .await
2715            .expect("quiet session should receive wake before timeout")
2716            .expect("quiet wake channel should stay open");
2717        assert_eq!(*quiet_wake_rx.borrow(), 1);
2718
2719        drop(notifications_tx);
2720        drop(client);
2721        server.await.expect("server task should finish");
2722    }
2723}