Add process-owned code mode session

This commit is contained in:
Channing Conger
2026-06-25 01:00:54 +00:00
parent c52c04ba7b
commit 464273e64d
15 changed files with 1942 additions and 2 deletions
+3
View File
@@ -1,9 +1,12 @@
mod cell_actor;
mod remote_session;
mod runtime;
mod service;
mod session_runtime;
pub use codex_code_mode_protocol::*;
pub use remote_session::ProcessOwnedCodeModeSession;
pub use remote_session::ProcessOwnedCodeModeSessionProvider;
pub use service::InProcessCodeModeSession;
pub use service::InProcessCodeModeSessionProvider;
pub use service::NoopCodeModeSessionDelegate;
+297
View File
@@ -0,0 +1,297 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeSession;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::CodeModeSessionProvider;
use codex_code_mode_protocol::CodeModeSessionProviderFuture;
use codex_code_mode_protocol::CodeModeSessionResultFuture;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::StartedCell;
use codex_code_mode_protocol::WaitOutcome;
use codex_code_mode_protocol::WaitRequest;
use codex_code_mode_protocol::host::SessionId;
use tokio::sync::Semaphore;
use self::connection::Connection;
use crate::NoopCodeModeSessionDelegate;
mod connection;
const CODE_MODE_HOST_PATH_ENV: &str = "CODEX_CODE_MODE_HOST_PATH";
/// Creates code-mode sessions backed by one lazily spawned process host.
pub struct ProcessOwnedCodeModeSessionProvider {
host_program: PathBuf,
process_host: StdMutex<Option<Arc<OwnedProcessHost>>>,
}
impl ProcessOwnedCodeModeSessionProvider {
pub fn with_host_program(host_program: PathBuf) -> Self {
Self {
host_program,
process_host: StdMutex::new(None),
}
}
fn process_host(&self) -> Arc<OwnedProcessHost> {
let mut process_host = self
.process_host
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(process_host) = process_host.as_ref() {
return Arc::clone(process_host);
}
let new_process_host = Arc::new(OwnedProcessHost::new(self.host_program.clone()));
*process_host = Some(Arc::clone(&new_process_host));
new_process_host
}
}
impl Default for ProcessOwnedCodeModeSessionProvider {
fn default() -> Self {
Self::with_host_program(default_host_program())
}
}
impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
fn create_session<'a>(
&'a self,
delegate: Arc<dyn CodeModeSessionDelegate>,
) -> CodeModeSessionProviderFuture<'a> {
let session = ProcessOwnedCodeModeSession::with_process_host(delegate, self.process_host());
Box::pin(async move {
session.connection().await?;
let session: Arc<dyn CodeModeSession> = Arc::new(session);
Ok(session)
})
}
}
struct OwnedProcessHost {
host_program: PathBuf,
connection: StdMutex<Option<Arc<Connection>>>,
spawn_permit: Semaphore,
next_session_id: AtomicU64,
}
impl OwnedProcessHost {
fn new(host_program: PathBuf) -> Self {
Self {
host_program,
connection: StdMutex::new(None),
spawn_permit: Semaphore::new(/*permits*/ 1),
next_session_id: AtomicU64::new(1),
}
}
async fn connection(&self) -> Result<Arc<Connection>, String> {
if let Some(connection) = self.live_connection() {
return Ok(connection);
}
let _spawn_permit = self
.spawn_permit
.acquire()
.await
.map_err(|_| "code-mode host spawn coordinator closed".to_string())?;
if let Some(connection) = self.live_connection() {
return Ok(connection);
}
let new_connection = Arc::new(Connection::spawn(&self.host_program).await?);
*self
.connection
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(Arc::clone(&new_connection));
Ok(new_connection)
}
fn live_connection(&self) -> Option<Arc<Connection>> {
self.connection
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.filter(|connection| connection.is_alive())
.cloned()
}
fn allocate_session_id(&self) -> SessionId {
let value = self.next_session_id.fetch_add(1, Ordering::Relaxed);
match SessionId::new(format!("session-{value}")) {
Ok(session_id) => session_id,
Err(_) => unreachable!("a generated code-mode session ID is nonempty"),
}
}
}
enum SessionState {
New,
Open(Arc<Connection>),
Shutdown,
}
/// A logical code-mode session assigned to a process-owned host.
pub struct ProcessOwnedCodeModeSession {
process_host: Arc<OwnedProcessHost>,
session_id: SessionId,
delegate: Arc<dyn CodeModeSessionDelegate>,
state: StdMutex<SessionState>,
transition_permit: Semaphore,
}
impl ProcessOwnedCodeModeSession {
pub fn new() -> Self {
Self::with_process_host(
Arc::new(NoopCodeModeSessionDelegate),
Arc::new(OwnedProcessHost::new(default_host_program())),
)
}
fn with_process_host(
delegate: Arc<dyn CodeModeSessionDelegate>,
process_host: Arc<OwnedProcessHost>,
) -> Self {
let session_id = process_host.allocate_session_id();
Self {
process_host,
session_id,
delegate,
state: StdMutex::new(SessionState::New),
transition_permit: Semaphore::new(/*permits*/ 1),
}
}
async fn connection(&self) -> Result<Arc<Connection>, String> {
if let Some(connection) = self.current_connection()? {
return Ok(connection);
}
let _transition_permit = self
.transition_permit
.acquire()
.await
.map_err(|_| "code-mode session transition coordinator closed".to_string())?;
if let Some(connection) = self.current_connection()? {
return Ok(connection);
}
let connection = self.process_host.connection().await?;
connection
.open_session(self.session_id.clone(), Arc::clone(&self.delegate))
.await?;
*self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
SessionState::Open(Arc::clone(&connection));
Ok(connection)
}
fn current_connection(&self) -> Result<Option<Arc<Connection>>, String> {
match &*self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
{
SessionState::New => Ok(None),
SessionState::Open(connection) => Ok(Some(Arc::clone(connection))),
SessionState::Shutdown => Err("code mode session is shutting down".to_string()),
}
}
pub async fn execute(&self, request: ExecuteRequest) -> Result<StartedCell, String> {
self.connection()
.await?
.execute(self.session_id.clone(), request)
.await
}
pub async fn wait(&self, request: WaitRequest) -> Result<WaitOutcome, String> {
self.connection()
.await?
.wait(self.session_id.clone(), request)
.await
}
pub async fn terminate(&self, cell_id: CellId) -> Result<WaitOutcome, String> {
self.connection()
.await?
.terminate(self.session_id.clone(), cell_id)
.await
}
pub async fn shutdown(&self) -> Result<(), String> {
let transition_permit = self
.transition_permit
.acquire()
.await
.map_err(|_| "code-mode session transition coordinator closed".to_string())?;
let connection = {
let mut state = self
.state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match std::mem::replace(&mut *state, SessionState::Shutdown) {
SessionState::Open(connection) => connection,
SessionState::New | SessionState::Shutdown => return Ok(()),
}
};
drop(transition_permit);
connection.shutdown_session(self.session_id.clone()).await
}
}
impl Default for ProcessOwnedCodeModeSession {
fn default() -> Self {
Self::new()
}
}
impl CodeModeSession for ProcessOwnedCodeModeSession {
fn execute<'a>(
&'a self,
request: ExecuteRequest,
) -> CodeModeSessionResultFuture<'a, StartedCell> {
Box::pin(ProcessOwnedCodeModeSession::execute(self, request))
}
fn wait<'a>(&'a self, request: WaitRequest) -> CodeModeSessionResultFuture<'a, WaitOutcome> {
Box::pin(ProcessOwnedCodeModeSession::wait(self, request))
}
fn terminate<'a>(&'a self, cell_id: CellId) -> CodeModeSessionResultFuture<'a, WaitOutcome> {
Box::pin(ProcessOwnedCodeModeSession::terminate(self, cell_id))
}
fn shutdown<'a>(&'a self) -> CodeModeSessionResultFuture<'a, ()> {
Box::pin(ProcessOwnedCodeModeSession::shutdown(self))
}
}
fn default_host_program() -> PathBuf {
if let Some(path) = std::env::var_os(CODE_MODE_HOST_PATH_ENV) {
return PathBuf::from(path);
}
let executable_name = if cfg!(windows) {
"codex-code-mode-host.exe"
} else {
"codex-code-mode-host"
};
if let Ok(current_exe) = std::env::current_exe()
&& let Some(parent) = current_exe.parent()
{
let sibling = parent.join(executable_name);
if sibling.is_file() {
return sibling;
}
}
PathBuf::from(executable_name)
}
#[cfg(test)]
#[path = "remote_session_tests.rs"]
mod tests;
@@ -0,0 +1,405 @@
use std::collections::HashMap;
use std::path::Path;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
use codex_code_mode_protocol::CellId;
use codex_code_mode_protocol::CodeModeSessionDelegate;
use codex_code_mode_protocol::ExecuteRequest;
use codex_code_mode_protocol::RuntimeResponse;
use codex_code_mode_protocol::StartedCell;
use codex_code_mode_protocol::WaitOutcome;
use codex_code_mode_protocol::WaitRequest;
use codex_code_mode_protocol::host::CapabilitySet;
use codex_code_mode_protocol::host::ClientHello;
use codex_code_mode_protocol::host::ClientToHost;
use codex_code_mode_protocol::host::DelegateRequestId;
use codex_code_mode_protocol::host::FramedReader;
use codex_code_mode_protocol::host::FramedWriter;
use codex_code_mode_protocol::host::HostRequest;
use codex_code_mode_protocol::host::HostResponse;
use codex_code_mode_protocol::host::HostToClient;
use codex_code_mode_protocol::host::ProtocolVersion;
use codex_code_mode_protocol::host::RequestId;
use codex_code_mode_protocol::host::SessionId;
use codex_code_mode_protocol::host::SupportedProtocolVersions;
use tokio::io::AsyncBufReadExt;
use tokio::io::BufReader;
use tokio::process::Command;
use tokio::sync::Mutex;
use tokio::sync::mpsc;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use tracing::debug;
use tracing::warn;
use self::reader::drive_reader;
mod reader;
const IPC_CHANNEL_CAPACITY: usize = 128;
pub(super) struct Connection {
state: Arc<ConnectionState>,
cancellation: CancellationToken,
}
impl Connection {
pub(super) async fn spawn(host_program: &Path) -> Result<Self, String> {
let mut command = Command::new(host_program);
#[cfg(unix)]
command.process_group(0);
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|err| {
format!(
"failed to spawn code-mode host {}: {err}",
host_program.display()
)
})?;
let stdin = child
.stdin
.take()
.ok_or_else(|| "spawned code-mode host has no stdin".to_string())?;
let stdout = child
.stdout
.take()
.ok_or_else(|| "spawned code-mode host has no stdout".to_string())?;
let stderr = child.stderr.take();
let mut reader = FramedReader::new(stdout);
let mut writer = FramedWriter::new(stdin);
let hello = ClientHello::new(
SupportedProtocolVersions::try_new([ProtocolVersion::V1])
.map_err(|err| err.to_string())?,
CapabilitySet::empty(),
CapabilitySet::empty(),
)
.map_err(|err| err.to_string())?;
writer
.write(&ClientToHost::ClientHello(hello))
.await
.map_err(|err| format!("failed to write code-mode host hello: {err}"))?;
match reader
.read::<HostToClient>()
.await
.map_err(|err| format!("failed to read code-mode host hello: {err}"))?
{
Some(HostToClient::HostHello(hello))
if hello.selected_version() == ProtocolVersion::V1 => {}
Some(HostToClient::HandshakeRejected { reason }) => {
return Err(format!("code-mode host rejected the handshake: {reason:?}"));
}
Some(message) => {
return Err(format!(
"code-mode host returned an invalid handshake response: {message:?}"
));
}
None => return Err("code-mode host exited during handshake".to_string()),
}
let (outgoing_tx, mut outgoing_rx) = mpsc::channel(IPC_CHANNEL_CAPACITY);
let cancellation = CancellationToken::new();
let state = Arc::new(ConnectionState::new(outgoing_tx, cancellation.clone()));
let writer_state = Arc::downgrade(&state);
let writer_cancellation = cancellation.clone();
tokio::spawn(async move {
loop {
tokio::select! {
_ = writer_cancellation.cancelled() => break,
message = outgoing_rx.recv() => {
let Some(message) = message else {
break;
};
if let Err(err) = writer.write(&message).await {
if let Some(state) = writer_state.upgrade() {
state.fail(format!(
"failed to write code-mode host message: {err}"
)).await;
}
break;
}
}
}
}
});
let reader_state = Arc::downgrade(&state);
let reader_cancellation = cancellation.clone();
tokio::spawn(async move {
drive_reader(reader, reader_state, reader_cancellation).await;
});
if let Some(stderr) = stderr {
tokio::spawn(async move {
let mut lines = BufReader::new(stderr).lines();
loop {
match lines.next_line().await {
Ok(Some(line)) => debug!("code-mode host stderr: {line}"),
Ok(None) => break,
Err(err) => {
warn!("failed to read code-mode host stderr: {err}");
break;
}
}
}
});
}
let supervisor_state = Arc::downgrade(&state);
let supervisor_cancellation = cancellation.clone();
tokio::spawn(async move {
tokio::select! {
result = child.wait() => {
let reason = match result {
Ok(status) => format!("code-mode host exited with status {status}"),
Err(err) => format!("failed waiting for code-mode host: {err}"),
};
if let Some(state) = supervisor_state.upgrade() {
state.fail(reason).await;
}
}
_ = supervisor_cancellation.cancelled() => {
let _ = child.start_kill();
let _ = child.wait().await;
}
}
});
Ok(Self {
state,
cancellation,
})
}
pub(super) fn is_alive(&self) -> bool {
self.state.alive.load(Ordering::Acquire)
}
pub(super) async fn open_session(
&self,
session_id: SessionId,
delegate: Arc<dyn CodeModeSessionDelegate>,
) -> Result<(), String> {
self.state
.delegates
.lock()
.await
.insert(session_id.clone(), delegate);
let response = self
.request(HostRequest::OpenSession {
session_id: session_id.clone(),
})
.await;
match response {
Ok(HostResponse::SessionReady {
session_id: ready_session_id,
}) if ready_session_id == session_id => Ok(()),
Ok(_) => {
self.state.delegates.lock().await.remove(&session_id);
Err("code-mode host returned an invalid open-session response".to_string())
}
Err(err) => {
self.state.delegates.lock().await.remove(&session_id);
Err(err)
}
}
}
pub(super) async fn execute(
&self,
session_id: SessionId,
request: ExecuteRequest,
) -> Result<StartedCell, String> {
let request = request
.try_into()
.map_err(|err| format!("failed to encode code-mode execute request: {err}"))?;
let id = RequestId::new(self.state.next_request_id.fetch_add(1, Ordering::Relaxed));
let (response_tx, response_rx) = oneshot::channel();
let (initial_tx, initial_rx) = oneshot::channel();
self.state.pending.lock().await.insert(id, response_tx);
self.state
.initial_responses
.lock()
.await
.insert(id, initial_tx);
if let Err(err) = self
.state
.send(ClientToHost::Request {
id,
request: HostRequest::Execute {
session_id,
request,
},
})
.await
{
self.state.pending.lock().await.remove(&id);
self.state.initial_responses.lock().await.remove(&id);
return Err(err);
}
let response = match response_rx.await {
Ok(Ok(response)) => response,
Ok(Err(err)) => {
self.state.initial_responses.lock().await.remove(&id);
return Err(err);
}
Err(_) => {
self.state.initial_responses.lock().await.remove(&id);
return Err(self.state.failure_message());
}
};
match response {
HostResponse::ExecutionStarted { cell_id } => Ok(StartedCell::from_result_receiver(
cell_id.into(),
initial_rx,
)),
_ => {
self.state.initial_responses.lock().await.remove(&id);
Err("code-mode host returned an invalid execute response".to_string())
}
}
}
pub(super) async fn wait(
&self,
session_id: SessionId,
request: WaitRequest,
) -> Result<WaitOutcome, String> {
match self
.request(HostRequest::Wait {
session_id,
request: request.into(),
})
.await?
{
HostResponse::WaitCompleted { outcome } => Ok(outcome.into()),
_ => Err("code-mode host returned an invalid wait response".to_string()),
}
}
pub(super) async fn terminate(
&self,
session_id: SessionId,
cell_id: CellId,
) -> Result<WaitOutcome, String> {
match self
.request(HostRequest::Terminate {
session_id,
cell_id: cell_id.into(),
})
.await?
{
HostResponse::WaitCompleted { outcome } => Ok(outcome.into()),
_ => Err("code-mode host returned an invalid terminate response".to_string()),
}
}
pub(super) async fn shutdown_session(&self, session_id: SessionId) -> Result<(), String> {
let response = self
.request(HostRequest::ShutdownSession {
session_id: session_id.clone(),
})
.await;
self.state.delegates.lock().await.remove(&session_id);
match response? {
HostResponse::SessionClosed {
session_id: closed_session_id,
} if closed_session_id == session_id => Ok(()),
_ => Err("code-mode host returned an invalid shutdown response".to_string()),
}
}
async fn request(&self, request: HostRequest) -> Result<HostResponse, String> {
let id = RequestId::new(self.state.next_request_id.fetch_add(1, Ordering::Relaxed));
let (response_tx, response_rx) = oneshot::channel();
self.state.pending.lock().await.insert(id, response_tx);
if let Err(err) = self.state.send(ClientToHost::Request { id, request }).await {
self.state.pending.lock().await.remove(&id);
return Err(err);
}
response_rx
.await
.map_err(|_| self.state.failure_message())?
}
}
impl Drop for Connection {
fn drop(&mut self) {
self.cancellation.cancel();
}
}
struct ConnectionState {
outgoing_tx: mpsc::Sender<ClientToHost>,
pending: Mutex<HashMap<RequestId, oneshot::Sender<Result<HostResponse, String>>>>,
initial_responses: Mutex<HashMap<RequestId, oneshot::Sender<Result<RuntimeResponse, String>>>>,
delegates: Mutex<HashMap<SessionId, Arc<dyn CodeModeSessionDelegate>>>,
delegate_cancellations: Mutex<HashMap<DelegateRequestId, CancellationToken>>,
next_request_id: AtomicI64,
alive: AtomicBool,
failure: std::sync::Mutex<Option<String>>,
cancellation: CancellationToken,
}
impl ConnectionState {
fn new(outgoing_tx: mpsc::Sender<ClientToHost>, cancellation: CancellationToken) -> Self {
Self {
outgoing_tx,
pending: Mutex::new(HashMap::new()),
initial_responses: Mutex::new(HashMap::new()),
delegates: Mutex::new(HashMap::new()),
delegate_cancellations: Mutex::new(HashMap::new()),
next_request_id: AtomicI64::new(1),
alive: AtomicBool::new(true),
failure: std::sync::Mutex::new(None),
cancellation,
}
}
async fn send(&self, message: ClientToHost) -> Result<(), String> {
if !self.alive.load(Ordering::Acquire) {
return Err(self.failure_message());
}
self.outgoing_tx
.send(message)
.await
.map_err(|_| self.failure_message())
}
fn failure_message(&self) -> String {
self.failure
.lock()
.ok()
.and_then(|failure| failure.clone())
.unwrap_or_else(|| "code-mode host connection closed".to_string())
}
async fn fail(&self, reason: String) {
if !self.alive.swap(false, Ordering::AcqRel) {
return;
}
if let Ok(mut failure) = self.failure.lock() {
*failure = Some(reason.clone());
}
self.cancellation.cancel();
for (_, sender) in self.pending.lock().await.drain() {
let _ = sender.send(Err(reason.clone()));
}
for (_, sender) in self.initial_responses.lock().await.drain() {
let _ = sender.send(Err(reason.clone()));
}
self.delegates.lock().await.clear();
for (_, cancellation) in self.delegate_cancellations.lock().await.drain() {
cancellation.cancel();
}
}
}
@@ -0,0 +1,131 @@
use std::sync::Arc;
use std::sync::Weak;
use codex_code_mode_protocol::host::ClientToHost;
use codex_code_mode_protocol::host::DelegateRequest;
use codex_code_mode_protocol::host::DelegateResponse;
use codex_code_mode_protocol::host::FramedReader;
use codex_code_mode_protocol::host::HostToClient;
use codex_code_mode_protocol::host::WireResult;
use tokio::process::ChildStdout;
use tokio_util::sync::CancellationToken;
use super::ConnectionState;
pub(super) async fn drive_reader(
mut reader: FramedReader<ChildStdout>,
state: Weak<ConnectionState>,
cancellation: CancellationToken,
) {
loop {
let message = tokio::select! {
_ = cancellation.cancelled() => return,
result = reader.read::<HostToClient>() => result,
};
match message {
Ok(Some(message)) => {
let Some(state) = state.upgrade() else {
return;
};
handle_host_message(state, message).await;
}
Ok(None) => {
if let Some(state) = state.upgrade() {
state
.fail("code-mode host closed its stdout".to_string())
.await;
}
return;
}
Err(err) => {
if let Some(state) = state.upgrade() {
state
.fail(format!("failed to read code-mode host message: {err}"))
.await;
}
return;
}
}
}
}
async fn handle_host_message(state: Arc<ConnectionState>, message: HostToClient) {
match message {
HostToClient::Response { id, result } => {
if let Some(sender) = state.pending.lock().await.remove(&id) {
let _ = sender.send(result.into_result());
}
}
HostToClient::InitialResponse { id, result } => {
if let Some(sender) = state.initial_responses.lock().await.remove(&id) {
let _ = sender.send(result.into_result().map(Into::into));
}
}
HostToClient::DelegateRequest {
id,
session_id,
request,
} => {
let delegate = state.delegates.lock().await.get(&session_id).cloned();
let Some(delegate) = delegate else {
let _ = state
.send(ClientToHost::DelegateResponse {
id,
result: WireResult::Err {
message: format!("unknown code-mode session {session_id}"),
},
})
.await;
return;
};
let cancellation = CancellationToken::new();
state
.delegate_cancellations
.lock()
.await
.insert(id, cancellation.clone());
tokio::spawn(async move {
let result = match request {
DelegateRequest::InvokeTool { invocation } => delegate
.invoke_tool(invocation.into(), cancellation)
.await
.map(|result| DelegateResponse::ToolResult { result }),
DelegateRequest::Notify {
call_id,
cell_id,
text,
} => delegate
.notify(call_id, cell_id.into(), text, cancellation)
.await
.map(|()| DelegateResponse::NotificationDelivered),
};
state.delegate_cancellations.lock().await.remove(&id);
let _ = state
.send(ClientToHost::DelegateResponse {
id,
result: WireResult::from_result(result),
})
.await;
});
}
HostToClient::CancelDelegateRequest { id } => {
if let Some(cancellation) = state.delegate_cancellations.lock().await.remove(&id) {
cancellation.cancel();
}
}
HostToClient::CellClosed {
session_id,
cell_id,
} => {
if let Some(delegate) = state.delegates.lock().await.get(&session_id).cloned() {
let cell_id = cell_id.into();
delegate.cell_closed(&cell_id);
}
}
HostToClient::HostHello(_) | HostToClient::HandshakeRejected { .. } => {
state
.fail("code-mode host sent a second handshake response".to_string())
.await;
}
}
}
@@ -0,0 +1,52 @@
use std::sync::Arc;
use codex_code_mode_protocol::CodeModeSessionProvider;
use super::ProcessOwnedCodeModeSession;
use super::ProcessOwnedCodeModeSessionProvider;
use crate::NoopCodeModeSessionDelegate;
#[test]
fn provider_reuses_its_live_process_host() {
let provider = ProcessOwnedCodeModeSessionProvider::default();
let first = provider.process_host();
let second = provider.process_host();
assert!(Arc::ptr_eq(&first, &second));
}
#[tokio::test]
async fn provider_reports_host_spawn_failure() {
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
"codex-code-mode-host-does-not-exist".into(),
);
let error = provider
.create_session(Arc::new(NoopCodeModeSessionDelegate))
.await
.err()
.expect("session creation should fail");
assert!(error.contains("failed to spawn code-mode host"));
}
#[tokio::test]
async fn shutdown_before_open_does_not_spawn_the_host() {
let session = ProcessOwnedCodeModeSession::new();
session.shutdown().await.expect("shutdown session");
let error = session
.execute(codex_code_mode_protocol::ExecuteRequest {
tool_call_id: "call-1".to_string(),
enabled_tools: Vec::new(),
source: "text('unreachable')".to_string(),
yield_time_ms: None,
max_output_tokens: None,
})
.await
.err()
.expect("shutdown session should reject execution");
assert_eq!(error, "code mode session is shutting down");
}