use std::collections::HashMap; use std::sync::Arc; use std::sync::atomic::AtomicI64; use std::sync::atomic::Ordering; use codex_app_server::AppServerClientMessage; use codex_app_server::AppServerEventNotification; use codex_app_server::AppServerMessage; use codex_app_server::spawn_in_memory_typed; use codex_app_server_protocol::ClientNotification; use codex_app_server_protocol::ClientRequest; use codex_app_server_protocol::CommandExecutionApprovalDecision; use codex_app_server_protocol::CommandExecutionRequestApprovalResponse; use codex_app_server_protocol::DynamicToolCallResponse; use codex_app_server_protocol::ElicitationAction as V2ElicitationAction; use codex_app_server_protocol::FileChangeApprovalDecision; use codex_app_server_protocol::FileChangeRequestApprovalResponse; use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::Result as JsonResult; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ToolRequestUserInputParams; use codex_app_server_protocol::ToolRequestUserInputResponse; use codex_core::config::Config; use codex_core::config_loader::LoaderOverrides; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; use codex_core::protocol::ReviewDecision; use codex_feedback::CodexFeedback; use codex_protocol::ThreadId; use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse; use mcp_types::RequestId as McpRequestId; use tokio::sync::Mutex; use tokio::sync::mpsc; use tokio::sync::oneshot; use crate::app_event::AppEvent; use crate::app_event_sender::AppEventSender; type PendingResponseMap = HashMap>>; pub(crate) struct PendingResponse { receiver: oneshot::Receiver>, } impl PendingResponse { pub async fn into_typed( self, ) -> std::result::Result { let value = self .receiver .await .map_err(|_| internal_error("response channel closed"))??; serde_json::from_value(value).map_err(|err| internal_error(err.to_string())) } pub async fn discard(self) -> std::result::Result<(), JSONRPCErrorError> { let _ = self .receiver .await .map_err(|_| internal_error("response channel closed"))??; Ok(()) } } #[derive(Default)] struct PendingServerRequests { exec: HashMap, patch: HashMap, user_input: HashMap, } #[derive(Default)] struct QueuedResponses { exec: HashMap, patch: HashMap, user_input: HashMap, } #[derive(Default)] struct ElicitationState { thread_by_request: HashMap, queued: HashMap, } #[derive(Default)] struct TurnState { current_turn_by_thread: HashMap, } pub(crate) struct AppServerClient { sender: mpsc::Sender, pending: Arc>, pending_server_requests: Arc>, queued_responses: Arc>, elicitation_state: Arc>, turn_state: Arc>, next_request_id: Arc, } impl AppServerClient { pub(crate) fn spawn( app_event_tx: AppEventSender, config: Arc, cli_overrides: Vec<(String, toml::Value)>, loader_overrides: LoaderOverrides, feedback: CodexFeedback, config_warnings: Vec, session_source: codex_protocol::protocol::SessionSource, ) -> Self { let in_process = spawn_in_memory_typed( config.codex_linux_sandbox_exe.clone(), config, cli_overrides, loader_overrides, feedback, config_warnings, session_source, ); let client = Self { sender: in_process.incoming, pending: Arc::new(Mutex::new(HashMap::new())), pending_server_requests: Arc::new(Mutex::new(PendingServerRequests::default())), queued_responses: Arc::new(Mutex::new(QueuedResponses::default())), elicitation_state: Arc::new(Mutex::new(ElicitationState::default())), turn_state: Arc::new(Mutex::new(TurnState::default())), next_request_id: Arc::new(AtomicI64::new(1)), }; client.spawn_outgoing_handler(app_event_tx, in_process.outgoing); client } pub(crate) async fn request( &self, build: impl FnOnce(RequestId) -> ClientRequest, ) -> std::result::Result { let request_id = RequestId::Integer(self.next_request_id.fetch_add(1, Ordering::Relaxed)); let (tx, rx) = oneshot::channel(); self.pending.lock().await.insert(request_id.clone(), tx); if self .sender .send(AppServerClientMessage::Request(build(request_id.clone()))) .await .is_err() { self.pending.lock().await.remove(&request_id); return Err(internal_error("app-server request channel closed")); } Ok(PendingResponse { receiver: rx }) } pub(crate) async fn send_notification( &self, notification: ClientNotification, ) -> std::result::Result<(), JSONRPCErrorError> { self.sender .send(AppServerClientMessage::Notification(notification)) .await .map_err(|_| internal_error("app-server notification channel closed")) } pub(crate) async fn respond_exec_approval( &self, call_id: String, decision: ReviewDecision, ) -> std::result::Result<(), JSONRPCErrorError> { let request_id = { let mut pending = self.pending_server_requests.lock().await; pending.exec.remove(&call_id) }; let Some(request_id) = request_id else { self.queued_responses .lock() .await .exec .insert(call_id, decision); return Ok(()); }; let response = CommandExecutionRequestApprovalResponse { decision: map_exec_decision(decision), }; self.send_response(request_id, response).await } pub(crate) async fn respond_patch_approval( &self, call_id: String, decision: ReviewDecision, ) -> std::result::Result<(), JSONRPCErrorError> { let request_id = { let mut pending = self.pending_server_requests.lock().await; pending.patch.remove(&call_id) }; let Some(request_id) = request_id else { self.queued_responses .lock() .await .patch .insert(call_id, decision); return Ok(()); }; let response = FileChangeRequestApprovalResponse { decision: map_patch_decision(decision), }; self.send_response(request_id, response).await } pub(crate) async fn respond_user_input( &self, call_id: String, response: CoreRequestUserInputResponse, ) -> std::result::Result<(), JSONRPCErrorError> { let request_id = { let mut pending = self.pending_server_requests.lock().await; pending.user_input.remove(&call_id) }; let Some(request_id) = request_id else { self.queued_responses .lock() .await .user_input .insert(call_id, response); return Ok(()); }; let response = ToolRequestUserInputResponse { answers: response .answers .into_iter() .map(|(id, answer)| { ( id, codex_app_server_protocol::ToolRequestUserInputAnswer { answers: answer.answers, }, ) }) .collect(), }; self.send_response(request_id, response).await } pub(crate) async fn respond_elicitation( &self, server_name: String, request_id: McpRequestId, decision: codex_core::protocol::ElicitationAction, ) -> std::result::Result<(), JSONRPCErrorError> { let thread_id = { let mut state = self.elicitation_state.lock().await; state.thread_by_request.remove(&request_id) }; let Some(thread_id) = thread_id else { self.elicitation_state .lock() .await .queued .insert(request_id, (server_name, decision)); return Ok(()); }; let params = codex_app_server_protocol::McpElicitationResolveParams { thread_id: thread_id.to_string(), server_name, request_id: request_id.clone(), decision: V2ElicitationAction::from(decision), }; self.request(|id| ClientRequest::McpElicitationResolve { request_id: id, params, }) .await? .discard() .await } pub(crate) async fn interrupt_current_turn( &self, thread_id: ThreadId, ) -> std::result::Result<(), JSONRPCErrorError> { let turn_id = { let state = self.turn_state.lock().await; state.current_turn_by_thread.get(&thread_id).cloned() }; let Some(turn_id) = turn_id else { return Ok(()); }; let params = codex_app_server_protocol::TurnInterruptParams { thread_id: thread_id.to_string(), turn_id, }; self.request(|id| ClientRequest::TurnInterrupt { request_id: id, params, }) .await? .discard() .await } async fn send_response( &self, request_id: RequestId, response: T, ) -> std::result::Result<(), JSONRPCErrorError> { let result = serde_json::to_value(response).map_err(|err| internal_error(err.to_string()))?; self.sender .send(AppServerClientMessage::Response { id: request_id, result, }) .await .map_err(|_| internal_error("app-server response channel closed")) } fn spawn_outgoing_handler( &self, app_event_tx: AppEventSender, mut outgoing: mpsc::Receiver, ) { let pending = Arc::clone(&self.pending); let pending_server_requests = Arc::clone(&self.pending_server_requests); let queued_responses = Arc::clone(&self.queued_responses); let elicitation_state = Arc::clone(&self.elicitation_state); let turn_state = Arc::clone(&self.turn_state); let sender = self.sender.clone(); let next_request_id = Arc::clone(&self.next_request_id); tokio::spawn(async move { while let Some(message) = outgoing.recv().await { match message { AppServerMessage::EventNotification(notification) => { handle_event_notification( notification, &app_event_tx, &turn_state, &elicitation_state, &sender, next_request_id.as_ref(), ) .await; } AppServerMessage::Request(request) => { handle_server_request( request, &pending_server_requests, &queued_responses, &sender, ) .await; } AppServerMessage::Response { id, result } => { if let Some(tx) = pending.lock().await.remove(&id) { let _ = tx.send(Ok(result)); } } AppServerMessage::Error { id, error } => { if let Some(tx) = pending.lock().await.remove(&id) { let _ = tx.send(Err(error)); } } AppServerMessage::Notification(_notification) => { // v2 notifications are currently surfaced through codex/event // for the TUI, so ignore explicit server notifications here. } } } }); } } async fn handle_event_notification( notification: AppServerEventNotification, app_event_tx: &AppEventSender, turn_state: &Mutex, elicitation_state: &Mutex, sender: &mpsc::Sender, next_request_id: &AtomicI64, ) { if !notification.method.starts_with("codex/event/") { return; } let Some(params) = notification.params else { return; }; let serde_json::Value::Object(mut map) = params else { return; }; let Some(conversation_id) = map.remove("conversationId") else { return; }; let thread_id = match conversation_id.as_str() { Some(value) => match ThreadId::from_string(value) { Ok(thread_id) => thread_id, Err(err) => { tracing::warn!("invalid thread id in event: {err}"); return; } }, None => return, }; let event_value = serde_json::Value::Object(map); let event: Event = match serde_json::from_value(event_value) { Ok(event) => event, Err(err) => { tracing::warn!("failed to parse codex event: {err}"); return; } }; match &event.msg { EventMsg::TurnStarted(_) => { if !event.id.is_empty() { turn_state .lock() .await .current_turn_by_thread .insert(thread_id, event.id.clone()); } } EventMsg::TurnComplete(_) | EventMsg::TurnAborted(_) => { if !event.id.is_empty() { let mut state = turn_state.lock().await; if state .current_turn_by_thread .get(&thread_id) .is_some_and(|id| id == &event.id) { state.current_turn_by_thread.remove(&thread_id); } } } EventMsg::ElicitationRequest(ev) => { let mut state = elicitation_state.lock().await; state.thread_by_request.insert(ev.id.clone(), thread_id); if let Some((server_name, decision)) = state.queued.remove(&ev.id) { let params = codex_app_server_protocol::McpElicitationResolveParams { thread_id: thread_id.to_string(), server_name, request_id: ev.id.clone(), decision: V2ElicitationAction::from(decision), }; let request_id = RequestId::Integer(next_request_id.fetch_add(1, Ordering::Relaxed)); let request = ClientRequest::McpElicitationResolve { request_id, params }; let _ = sender.send(AppServerClientMessage::Request(request)).await; } } _ => {} } app_event_tx.send(AppEvent::CodexThreadEvent { thread_id, event }); } async fn handle_server_request( request: ServerRequest, pending: &Mutex, queued: &Mutex, sender: &mpsc::Sender, ) { match request { ServerRequest::CommandExecutionRequestApproval { request_id, params } => { record_server_request( request_id, params.item_id, &mut pending.lock().await.exec, &mut queued.lock().await.exec, |decision| CommandExecutionRequestApprovalResponse { decision: map_exec_decision(decision), }, sender, ) .await; } ServerRequest::FileChangeRequestApproval { request_id, params } => { record_server_request( request_id, params.item_id, &mut pending.lock().await.patch, &mut queued.lock().await.patch, |decision| FileChangeRequestApprovalResponse { decision: map_patch_decision(decision), }, sender, ) .await; } ServerRequest::ToolRequestUserInput { request_id, params } => { record_user_input_request(request_id, params, pending, queued, sender).await; } ServerRequest::DynamicToolCall { request_id, params } => { let response = DynamicToolCallResponse { output: "Dynamic tools are not supported in the TUI yet.".to_string(), success: false, }; let _ = send_response(sender, request_id, response).await; tracing::warn!( "dynamic tool call {} for tool {} ignored", params.call_id, params.tool ); } _ => {} } } async fn record_user_input_request( request_id: RequestId, params: ToolRequestUserInputParams, pending: &Mutex, queued: &Mutex, sender: &mpsc::Sender, ) { let call_id = params.item_id; let mut pending_guard = pending.lock().await; if let Some(response) = queued.lock().await.user_input.remove(&call_id) { drop(pending_guard); let response = ToolRequestUserInputResponse { answers: response .answers .into_iter() .map(|(id, answer)| { ( id, codex_app_server_protocol::ToolRequestUserInputAnswer { answers: answer.answers, }, ) }) .collect(), }; let _ = send_response(sender, request_id, response).await; } else { pending_guard.user_input.insert(call_id, request_id); } } async fn record_server_request( request_id: RequestId, call_id: String, pending: &mut HashMap, queued: &mut HashMap, build_response: impl FnOnce(ReviewDecision) -> T, sender: &mpsc::Sender, ) { if let Some(decision) = queued.remove(&call_id) { let response = build_response(decision); let _ = send_response(sender, request_id, response).await; } else { pending.insert(call_id, request_id); } } async fn send_response( sender: &mpsc::Sender, request_id: RequestId, response: T, ) -> std::result::Result<(), JSONRPCErrorError> { let result = serde_json::to_value(response).map_err(|err| internal_error(err.to_string()))?; sender .send(AppServerClientMessage::Response { id: request_id, result, }) .await .map_err(|_| internal_error("app-server response channel closed")) } fn internal_error(message: impl Into) -> JSONRPCErrorError { JSONRPCErrorError { code: -32603, message: message.into(), data: None, } } fn map_exec_decision(decision: ReviewDecision) -> CommandExecutionApprovalDecision { match decision { ReviewDecision::Approved => CommandExecutionApprovalDecision::Accept, ReviewDecision::ApprovedForSession => CommandExecutionApprovalDecision::AcceptForSession, ReviewDecision::ApprovedExecpolicyAmendment { proposed_execpolicy_amendment, } => CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment { execpolicy_amendment: proposed_execpolicy_amendment.into(), }, ReviewDecision::Denied => CommandExecutionApprovalDecision::Decline, ReviewDecision::Abort => CommandExecutionApprovalDecision::Cancel, } } fn map_patch_decision(decision: ReviewDecision) -> FileChangeApprovalDecision { match decision { ReviewDecision::Approved => FileChangeApprovalDecision::Accept, ReviewDecision::ApprovedForSession => FileChangeApprovalDecision::AcceptForSession, ReviewDecision::ApprovedExecpolicyAmendment { .. } => FileChangeApprovalDecision::Accept, ReviewDecision::Denied => FileChangeApprovalDecision::Decline, ReviewDecision::Abort => FileChangeApprovalDecision::Cancel, } }