Skip to content

Commit 085ffb4

Browse files
jif-oaicodex
andauthored
feat: move exec-server ownership (openai#16344)
This introduces session-scoped ownership for exec-server so ws disconnects no longer immediately kill running remote exec processes, and it prepares the protocol for reconnect-based resume. - add session_id / resume_session_id to the exec-server initialize handshake - move process ownership under a shared session registry - detach sessions on websocket disconnect and expire them after a TTL instead of killing processes immediately (we will resume based on this) - allow a new connection to resume an existing session and take over notifications/ownership - I use UUID to make them not predictable as we don't have auth for now - make detached-session expiry authoritative at resume time so teardown wins at the TTL boundary - reject long-poll process/read calls that get resumed out from under an older attachment --------- Co-authored-by: Codex <noreply@openai.com>
1 parent 7bbe3b6 commit 085ffb4

21 files changed

Lines changed: 1202 additions & 171 deletions

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/exec-server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ tokio = { workspace = true, features = [
4040
] }
4141
tokio-tungstenite = { workspace = true }
4242
tracing = { workspace = true }
43+
uuid = { workspace = true, features = ["v4"] }
4344

4445
[dev-dependencies]
4546
anyhow = { workspace = true }

codex-rs/exec-server/src/client.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ impl Default for ExecServerClientConnectOptions {
7171
Self {
7272
client_name: "codex-core".to_string(),
7373
initialize_timeout: INITIALIZE_TIMEOUT,
74+
resume_session_id: None,
7475
}
7576
}
7677
}
@@ -80,6 +81,7 @@ impl From<RemoteExecServerConnectArgs> for ExecServerClientConnectOptions {
8081
Self {
8182
client_name: value.client_name,
8283
initialize_timeout: value.initialize_timeout,
84+
resume_session_id: value.resume_session_id,
8385
}
8486
}
8587
}
@@ -91,6 +93,7 @@ impl RemoteExecServerConnectArgs {
9193
client_name,
9294
connect_timeout: CONNECT_TIMEOUT,
9395
initialize_timeout: INITIALIZE_TIMEOUT,
96+
resume_session_id: None,
9497
}
9598
}
9699
}
@@ -118,6 +121,7 @@ struct Inner {
118121
// need serialization so concurrent register/remove operations do not
119122
// overwrite each other's copy-on-write updates.
120123
sessions_write_lock: Mutex<()>,
124+
session_id: std::sync::RwLock<Option<String>>,
121125
reader_task: tokio::task::JoinHandle<()>,
122126
}
123127

@@ -190,14 +194,29 @@ impl ExecServerClient {
190194
let ExecServerClientConnectOptions {
191195
client_name,
192196
initialize_timeout,
197+
resume_session_id,
193198
} = options;
194199

195200
timeout(initialize_timeout, async {
196-
let response = self
201+
let response: InitializeResponse = self
197202
.inner
198203
.client
199-
.call(INITIALIZE_METHOD, &InitializeParams { client_name })
204+
.call(
205+
INITIALIZE_METHOD,
206+
&InitializeParams {
207+
client_name,
208+
resume_session_id,
209+
},
210+
)
200211
.await?;
212+
{
213+
let mut session_id = self
214+
.inner
215+
.session_id
216+
.write()
217+
.unwrap_or_else(std::sync::PoisonError::into_inner);
218+
*session_id = Some(response.session_id.clone());
219+
}
201220
self.notify_initialized().await?;
202221
Ok(response)
203222
})
@@ -350,6 +369,14 @@ impl ExecServerClient {
350369
self.inner.remove_session(process_id).await;
351370
}
352371

372+
pub fn session_id(&self) -> Option<String> {
373+
self.inner
374+
.session_id
375+
.read()
376+
.unwrap_or_else(std::sync::PoisonError::into_inner)
377+
.clone()
378+
}
379+
353380
async fn connect(
354381
connection: JsonRpcConnection,
355382
options: ExecServerClientConnectOptions,
@@ -388,6 +415,7 @@ impl ExecServerClient {
388415
client: rpc_client,
389416
sessions: ArcSwap::from_pointee(HashMap::new()),
390417
sessions_write_lock: Mutex::new(()),
418+
session_id: std::sync::RwLock::new(None),
391419
reader_task,
392420
}
393421
});
@@ -693,8 +721,10 @@ mod tests {
693721
&mut server_writer,
694722
JSONRPCMessage::Response(JSONRPCResponse {
695723
id: request.id,
696-
result: serde_json::to_value(InitializeResponse {})
697-
.expect("initialize response should serialize"),
724+
result: serde_json::to_value(InitializeResponse {
725+
session_id: "session-1".to_string(),
726+
})
727+
.expect("initialize response should serialize"),
698728
}),
699729
)
700730
.await;

codex-rs/exec-server/src/client_api.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use std::time::Duration;
55
pub struct ExecServerClientConnectOptions {
66
pub client_name: String,
77
pub initialize_timeout: Duration,
8+
pub resume_session_id: Option<String>,
89
}
910

1011
/// WebSocket connection arguments for a remote exec-server.
@@ -14,4 +15,5 @@ pub struct RemoteExecServerConnectArgs {
1415
pub client_name: String,
1516
pub connect_timeout: Duration,
1617
pub initialize_timeout: Duration,
18+
pub resume_session_id: Option<String>,
1719
}

codex-rs/exec-server/src/connection.rs

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use futures::StreamExt;
44
use tokio::io::AsyncRead;
55
use tokio::io::AsyncWrite;
66
use tokio::sync::mpsc;
7+
use tokio::sync::watch;
78
use tokio_tungstenite::WebSocketStream;
89
use tokio_tungstenite::tungstenite::Message;
910

@@ -28,6 +29,7 @@ pub(crate) enum JsonRpcConnectionEvent {
2829
pub(crate) struct JsonRpcConnection {
2930
outgoing_tx: mpsc::Sender<JSONRPCMessage>,
3031
incoming_rx: mpsc::Receiver<JsonRpcConnectionEvent>,
32+
disconnected_rx: watch::Receiver<bool>,
3133
task_handles: Vec<tokio::task::JoinHandle<()>>,
3234
}
3335

@@ -40,9 +42,11 @@ impl JsonRpcConnection {
4042
{
4143
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY);
4244
let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY);
45+
let (disconnected_tx, disconnected_rx) = watch::channel(false);
4346

4447
let reader_label = connection_label.clone();
4548
let incoming_tx_for_reader = incoming_tx.clone();
49+
let disconnected_tx_for_reader = disconnected_tx.clone();
4650
let reader_task = tokio::spawn(async move {
4751
let mut lines = BufReader::new(reader).lines();
4852
loop {
@@ -73,12 +77,18 @@ impl JsonRpcConnection {
7377
}
7478
}
7579
Ok(None) => {
76-
send_disconnected(&incoming_tx_for_reader, /*reason*/ None).await;
80+
send_disconnected(
81+
&incoming_tx_for_reader,
82+
&disconnected_tx_for_reader,
83+
/*reason*/ None,
84+
)
85+
.await;
7786
break;
7887
}
7988
Err(err) => {
8089
send_disconnected(
8190
&incoming_tx_for_reader,
91+
&disconnected_tx_for_reader,
8292
Some(format!(
8393
"failed to read JSON-RPC message from {reader_label}: {err}"
8494
)),
@@ -96,6 +106,7 @@ impl JsonRpcConnection {
96106
if let Err(err) = write_jsonrpc_line_message(&mut writer, &message).await {
97107
send_disconnected(
98108
&incoming_tx,
109+
&disconnected_tx,
99110
Some(format!(
100111
"failed to write JSON-RPC message to {connection_label}: {err}"
101112
)),
@@ -109,6 +120,7 @@ impl JsonRpcConnection {
109120
Self {
110121
outgoing_tx,
111122
incoming_rx,
123+
disconnected_rx,
112124
task_handles: vec![reader_task, writer_task],
113125
}
114126
}
@@ -119,10 +131,12 @@ impl JsonRpcConnection {
119131
{
120132
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(CHANNEL_CAPACITY);
121133
let (incoming_tx, incoming_rx) = mpsc::channel(CHANNEL_CAPACITY);
134+
let (disconnected_tx, disconnected_rx) = watch::channel(false);
122135
let (mut websocket_writer, mut websocket_reader) = stream.split();
123136

124137
let reader_label = connection_label.clone();
125138
let incoming_tx_for_reader = incoming_tx.clone();
139+
let disconnected_tx_for_reader = disconnected_tx.clone();
126140
let reader_task = tokio::spawn(async move {
127141
loop {
128142
match websocket_reader.next().await {
@@ -171,14 +185,20 @@ impl JsonRpcConnection {
171185
}
172186
}
173187
Some(Ok(Message::Close(_))) => {
174-
send_disconnected(&incoming_tx_for_reader, /*reason*/ None).await;
188+
send_disconnected(
189+
&incoming_tx_for_reader,
190+
&disconnected_tx_for_reader,
191+
/*reason*/ None,
192+
)
193+
.await;
175194
break;
176195
}
177196
Some(Ok(Message::Ping(_))) | Some(Ok(Message::Pong(_))) => {}
178197
Some(Ok(_)) => {}
179198
Some(Err(err)) => {
180199
send_disconnected(
181200
&incoming_tx_for_reader,
201+
&disconnected_tx_for_reader,
182202
Some(format!(
183203
"failed to read websocket JSON-RPC message from {reader_label}: {err}"
184204
)),
@@ -187,7 +207,12 @@ impl JsonRpcConnection {
187207
break;
188208
}
189209
None => {
190-
send_disconnected(&incoming_tx_for_reader, /*reason*/ None).await;
210+
send_disconnected(
211+
&incoming_tx_for_reader,
212+
&disconnected_tx_for_reader,
213+
/*reason*/ None,
214+
)
215+
.await;
191216
break;
192217
}
193218
}
@@ -202,6 +227,7 @@ impl JsonRpcConnection {
202227
{
203228
send_disconnected(
204229
&incoming_tx,
230+
&disconnected_tx,
205231
Some(format!(
206232
"failed to write websocket JSON-RPC message to {connection_label}: {err}"
207233
)),
@@ -213,6 +239,7 @@ impl JsonRpcConnection {
213239
Err(err) => {
214240
send_disconnected(
215241
&incoming_tx,
242+
&disconnected_tx,
216243
Some(format!(
217244
"failed to serialize JSON-RPC message for {connection_label}: {err}"
218245
)),
@@ -227,6 +254,7 @@ impl JsonRpcConnection {
227254
Self {
228255
outgoing_tx,
229256
incoming_rx,
257+
disconnected_rx,
230258
task_handles: vec![reader_task, writer_task],
231259
}
232260
}
@@ -236,16 +264,24 @@ impl JsonRpcConnection {
236264
) -> (
237265
mpsc::Sender<JSONRPCMessage>,
238266
mpsc::Receiver<JsonRpcConnectionEvent>,
267+
watch::Receiver<bool>,
239268
Vec<tokio::task::JoinHandle<()>>,
240269
) {
241-
(self.outgoing_tx, self.incoming_rx, self.task_handles)
270+
(
271+
self.outgoing_tx,
272+
self.incoming_rx,
273+
self.disconnected_rx,
274+
self.task_handles,
275+
)
242276
}
243277
}
244278

245279
async fn send_disconnected(
246280
incoming_tx: &mpsc::Sender<JsonRpcConnectionEvent>,
281+
disconnected_tx: &watch::Sender<bool>,
247282
reason: Option<String>,
248283
) {
284+
let _ = disconnected_tx.send(true);
249285
let _ = incoming_tx
250286
.send(JsonRpcConnectionEvent::Disconnected { reason })
251287
.await;

codex-rs/exec-server/src/environment.rs

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -105,18 +105,10 @@ pub struct Environment {
105105

106106
impl Default for Environment {
107107
fn default() -> Self {
108-
let local_process = LocalProcess::default();
109-
if let Err(err) = local_process.initialize() {
110-
panic!("default local process initialization should succeed: {err:?}");
111-
}
112-
if let Err(err) = local_process.initialized() {
113-
panic!("default local process should accept initialized notification: {err}");
114-
}
115-
116108
Self {
117109
exec_server_url: None,
118110
remote_exec_server_client: None,
119-
exec_backend: Arc::new(local_process),
111+
exec_backend: Arc::new(LocalProcess::default()),
120112
}
121113
}
122114
}
@@ -146,31 +138,20 @@ impl Environment {
146138
client_name: "codex-environment".to_string(),
147139
connect_timeout: std::time::Duration::from_secs(5),
148140
initialize_timeout: std::time::Duration::from_secs(5),
141+
resume_session_id: None,
149142
})
150143
.await?,
151144
)
152145
} else {
153146
None
154147
};
155148

156-
let exec_backend: Arc<dyn ExecBackend> = match remote_exec_server_client.clone() {
157-
Some(client) => Arc::new(RemoteProcess::new(client)),
158-
None if exec_server_url.is_some() => {
159-
return Err(ExecServerError::Protocol(
160-
"remote mode should have an exec-server client".to_string(),
161-
));
162-
}
163-
None => {
164-
let local_process = LocalProcess::default();
165-
local_process
166-
.initialize()
167-
.map_err(|err| ExecServerError::Protocol(err.message))?;
168-
local_process
169-
.initialized()
170-
.map_err(ExecServerError::Protocol)?;
171-
Arc::new(local_process)
172-
}
173-
};
149+
let exec_backend: Arc<dyn ExecBackend> =
150+
if let Some(client) = remote_exec_server_client.clone() {
151+
Arc::new(RemoteProcess::new(client))
152+
} else {
153+
Arc::new(LocalProcess::default())
154+
};
174155

175156
Ok(Self {
176157
exec_server_url,

0 commit comments

Comments
 (0)