From d6c4083f98094dbd8d86f70992729924d93f73c4 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Tue, 22 Jul 2025 15:54:33 -0700 Subject: [PATCH 01/14] feat: support dotenv (including ~/.codex/.env) (#1653) This PR adds a `load_dotenv()` helper function to the `codex-common` crate that is available when the `cli` feature is enabled. The function uses [`dotenvy`](https://crates.io/crates/dotenvy) to update the environment from: - `$CODEX_HOME/.env` - `$(pwd)/.env` To test: - ran `printenv OPENAI_API_KEY` to verify the env var exists in my environment - ran `just codex exec hello` to verify the CLI uses my `OPENAI_API_KEY` - ran `unset OPENAI_API_KEY` - ran `just codex exec hello` again and got **ERROR: Missing environment variable: `OPENAI_API_KEY`**, as expected - created `~/.codex/.env` and added `OPENAI_API_KEY=sk-proj-...` (also ran `chmod 400 ~/.codex/.env` for good measure) - ran `just codex exec hello` again and it worked, verifying it picked up `OPENAI_API_KEY` from `~/.codex/.env` Note this functionality was available in the TypeScript CLI: https://github.com/openai/codex/pull/122 and was recently requested over on https://github.com/openai/codex/issues/1262#issuecomment-3093203551. --- codex-rs/Cargo.lock | 8 ++++++++ codex-rs/core/src/config.rs | 2 +- codex-rs/linux-sandbox/Cargo.toml | 2 ++ codex-rs/linux-sandbox/src/lib.rs | 12 ++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9b4a4e32d4..3e4b84a435 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -756,7 +756,9 @@ version = "0.0.0" dependencies = [ "anyhow", "clap", + "codex-common", "codex-core", + "dotenvy", "landlock", "libc", "seccompiler", @@ -1272,6 +1274,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "dupe" version = "0.9.1" diff --git a/codex-rs/core/src/config.rs b/codex-rs/core/src/config.rs index 8ed06c45af..2dfd3e55fe 100644 --- a/codex-rs/core/src/config.rs +++ b/codex-rs/core/src/config.rs @@ -561,7 +561,7 @@ fn default_model() -> String { /// function will Err if the path does not exist. /// - If `CODEX_HOME` is not set, this function does not verify that the /// directory exists. -fn find_codex_home() -> std::io::Result { +pub fn find_codex_home() -> std::io::Result { // Honor the `CODEX_HOME` environment variable when it is set to allow users // (and tests) to override the default location. if let Ok(val) = std::env::var("CODEX_HOME") { diff --git a/codex-rs/linux-sandbox/Cargo.toml b/codex-rs/linux-sandbox/Cargo.toml index c8cd1078c0..5c2dea6083 100644 --- a/codex-rs/linux-sandbox/Cargo.toml +++ b/codex-rs/linux-sandbox/Cargo.toml @@ -17,7 +17,9 @@ workspace = true [dependencies] anyhow = "1" clap = { version = "4", features = ["derive"] } +codex-common = { path = "../common", features = ["cli"] } codex-core = { path = "../core" } +dotenvy = "0.15.7" tokio = { version = "1", features = ["rt-multi-thread"] } [dev-dependencies] diff --git a/codex-rs/linux-sandbox/src/lib.rs b/codex-rs/linux-sandbox/src/lib.rs index 568f015822..960678467c 100644 --- a/codex-rs/linux-sandbox/src/lib.rs +++ b/codex-rs/linux-sandbox/src/lib.rs @@ -43,6 +43,10 @@ where crate::run_main(); } + // This modifies the environment, which is not thread-safe, so do this + // before creating any threads/the Tokio runtime. + load_dotenv(); + // Regular invocation – create a Tokio runtime and execute the provided // async entry-point. let runtime = tokio::runtime::Runtime::new()?; @@ -61,3 +65,11 @@ where pub fn run_main() -> ! { panic!("codex-linux-sandbox is only supported on Linux"); } + +/// Load env vars from ~/.codex/.env and `$(pwd)/.env`. +fn load_dotenv() { + if let Ok(codex_home) = codex_core::config::find_codex_home() { + dotenvy::from_path(codex_home.join(".env")).ok(); + } + dotenvy::dotenv().ok(); +} From 591cb6149a1adfa7cb212d739f2fdd6b035072f4 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Wed, 23 Jul 2025 10:37:45 -0700 Subject: [PATCH 02/14] Always send entire request context (#1641) Always store the entire conversation history. Request encrypted COT when not storing Responses. Send entire input context instead of sending previous_response_id --- codex-rs/core/src/chat_completions.rs | 6 +- codex-rs/core/src/client.rs | 11 +- codex-rs/core/src/client_common.rs | 5 +- codex-rs/core/src/codex.rs | 173 +++++--------------- codex-rs/core/src/conversation_history.rs | 14 +- codex-rs/core/src/models.rs | 25 ++- codex-rs/core/src/rollout.rs | 27 +-- codex-rs/core/tests/previous_response_id.rs | 165 ------------------- 8 files changed, 101 insertions(+), 325 deletions(-) delete mode 100644 codex-rs/core/tests/previous_response_id.rs diff --git a/codex-rs/core/src/chat_completions.rs b/codex-rs/core/src/chat_completions.rs index 35045c8e1b..5adf3c4d50 100644 --- a/codex-rs/core/src/chat_completions.rs +++ b/codex-rs/core/src/chat_completions.rs @@ -41,7 +41,7 @@ pub(crate) async fn stream_chat_completions( for item in &prompt.input { match item { - ResponseItem::Message { role, content } => { + ResponseItem::Message { role, content, .. } => { let mut text = String::new(); for c in content { match c { @@ -58,6 +58,7 @@ pub(crate) async fn stream_chat_completions( name, arguments, call_id, + .. } => { messages.push(json!({ "role": "assistant", @@ -259,6 +260,7 @@ async fn process_chat_sse( content: vec![ContentItem::OutputText { text: content.to_string(), }], + id: None, }; let _ = tx_event.send(Ok(ResponseEvent::OutputItemDone(item))).await; @@ -300,6 +302,7 @@ async fn process_chat_sse( "tool_calls" if fn_call_state.active => { // Build the FunctionCall response item. let item = ResponseItem::FunctionCall { + id: None, name: fn_call_state.name.clone().unwrap_or_else(|| "".to_string()), arguments: fn_call_state.arguments.clone(), call_id: fn_call_state.call_id.clone().unwrap_or_else(String::new), @@ -402,6 +405,7 @@ where }))) => { if !this.cumulative.is_empty() { let aggregated_item = crate::models::ResponseItem::Message { + id: None, role: "assistant".to_string(), content: vec![crate::models::ContentItem::OutputText { text: std::mem::take(&mut this.cumulative), diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index beeaa453ad..1648da6d96 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -117,6 +117,15 @@ impl ModelClient { let full_instructions = prompt.get_full_instructions(&self.config.model); let tools_json = create_tools_json_for_responses_api(prompt, &self.config.model)?; let reasoning = create_reasoning_param_for_request(&self.config, self.effort, self.summary); + + // Request encrypted COT if we are not storing responses, + // otherwise reasoning items will be referenced by ID + let include = if !prompt.store && reasoning.is_some() { + vec!["reasoning.encrypted_content".to_string()] + } else { + vec![] + }; + let payload = ResponsesApiRequest { model: &self.config.model, instructions: &full_instructions, @@ -125,10 +134,10 @@ impl ModelClient { tool_choice: "auto", parallel_tool_calls: false, reasoning, - previous_response_id: prompt.prev_id.clone(), store: prompt.store, // TODO: make this configurable stream: true, + include, }; trace!( diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index 94d09e7fd4..afd2f04556 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -22,8 +22,6 @@ const BASE_INSTRUCTIONS: &str = include_str!("../prompt.md"); pub struct Prompt { /// Conversation context input items. pub input: Vec, - /// Optional previous response ID (when storage is enabled). - pub prev_id: Option, /// Optional instructions from the user to amend to the built-in agent /// instructions. pub user_instructions: Option, @@ -133,11 +131,10 @@ pub(crate) struct ResponsesApiRequest<'a> { pub(crate) tool_choice: &'static str, pub(crate) parallel_tool_calls: bool, pub(crate) reasoning: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) previous_response_id: Option, /// true when using the Responses API. pub(crate) store: bool, pub(crate) stream: bool, + pub(crate) include: Vec, } use crate::config::Config; diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 6eb1715fbf..ef2cc63bbb 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -34,7 +34,6 @@ use tracing::trace; use tracing::warn; use uuid::Uuid; -use crate::WireApi; use crate::client::ModelClient; use crate::client_common::Prompt; use crate::client_common::ResponseEvent; @@ -191,6 +190,7 @@ pub(crate) struct Session { sandbox_policy: SandboxPolicy, shell_environment_policy: ShellEnvironmentPolicy, writable_roots: Mutex>, + disable_response_storage: bool, /// Manager for external MCP servers/tools. mcp_connection_manager: McpConnectionManager, @@ -219,13 +219,9 @@ impl Session { struct State { approved_commands: HashSet>, current_task: Option, - /// Call IDs that have been sent from the Responses API but have not been sent back yet. - /// You CANNOT send a Responses API follow-up message unless you have sent back the output for all pending calls or else it will 400. - pending_call_ids: HashSet, - previous_response_id: Option, pending_approvals: HashMap>, pending_input: Vec, - zdr_transcript: Option, + history: ConversationHistory, } impl Session { @@ -320,18 +316,11 @@ impl Session { debug!("Recording items for conversation: {items:?}"); self.record_state_snapshot(items).await; - if let Some(transcript) = self.state.lock().unwrap().zdr_transcript.as_mut() { - transcript.record_items(items); - } + self.state.lock().unwrap().history.record_items(items); } async fn record_state_snapshot(&self, items: &[ResponseItem]) { - let snapshot = { - let state = self.state.lock().unwrap(); - crate::rollout::SessionStateSnapshot { - previous_response_id: state.previous_response_id.clone(), - } - }; + let snapshot = { crate::rollout::SessionStateSnapshot {} }; let recorder = { let guard = self.rollout.lock().unwrap(); @@ -433,8 +422,6 @@ impl Session { pub fn abort(&self) { info!("Aborting existing session"); let mut state = self.state.lock().unwrap(); - // Don't clear pending_call_ids because we need to keep track of them to ensure we don't 400 on the next turn. - // We will generate a synthetic aborted response for each pending call id. state.pending_approvals.clear(); state.pending_input.clear(); if let Some(task) = state.current_task.take() { @@ -479,15 +466,10 @@ impl Drop for Session { } impl State { - pub fn partial_clone(&self, retain_zdr_transcript: bool) -> Self { + pub fn partial_clone(&self) -> Self { Self { approved_commands: self.approved_commands.clone(), - previous_response_id: self.previous_response_id.clone(), - zdr_transcript: if retain_zdr_transcript { - self.zdr_transcript.clone() - } else { - None - }, + history: self.history.clone(), ..Default::default() } } @@ -606,13 +588,11 @@ async fn submission_loop( } // Optionally resume an existing rollout. let mut restored_items: Option> = None; - let mut restored_prev_id: Option = None; let rollout_recorder: Option = if let Some(path) = resume_path.as_ref() { match RolloutRecorder::resume(path).await { Ok((rec, saved)) => { session_id = saved.session_id; - restored_prev_id = saved.state.previous_response_id; if !saved.items.is_empty() { restored_items = Some(saved.items); } @@ -651,22 +631,13 @@ async fn submission_loop( ); // abort any current running session and clone its state - let retain_zdr_transcript = - record_conversation_history(disable_response_storage, provider.wire_api); let state = match sess.take() { Some(sess) => { sess.abort(); - sess.state - .lock() - .unwrap() - .partial_clone(retain_zdr_transcript) + sess.state.lock().unwrap().partial_clone() } None => State { - zdr_transcript: if retain_zdr_transcript { - Some(ConversationHistory::new()) - } else { - None - }, + history: ConversationHistory::new(), ..Default::default() }, }; @@ -717,18 +688,14 @@ async fn submission_loop( state: Mutex::new(state), rollout: Mutex::new(rollout_recorder), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), + disable_response_storage, })); // Patch restored state into the newly created session. if let Some(sess_arc) = &sess { - if restored_prev_id.is_some() || restored_items.is_some() { + if restored_items.is_some() { let mut st = sess_arc.state.lock().unwrap(); - st.previous_response_id = restored_prev_id; - if let (Some(hist), Some(items)) = - (st.zdr_transcript.as_mut(), restored_items.as_ref()) - { - hist.record_items(items.iter()); - } + st.history.record_items(restored_items.unwrap().iter()); } } @@ -875,14 +842,8 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { sess.record_conversation_items(&[initial_input_for_turn.clone().into()]) .await; - let mut input_for_next_turn: Vec = vec![initial_input_for_turn]; let last_agent_message: Option; loop { - let mut net_new_turn_input = input_for_next_turn - .drain(..) - .map(ResponseItem::from) - .collect::>(); - // Note that pending_input would be something like a message the user // submitted through the UI while the model was running. Though the UI // may support this, the model might not. @@ -899,29 +860,7 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { // only record the new items that originated in this turn so that it // represents an append-only log without duplicates. let turn_input: Vec = - if let Some(transcript) = sess.state.lock().unwrap().zdr_transcript.as_mut() { - // If we are using Chat/ZDR, we need to send the transcript with - // every turn. By induction, `transcript` already contains: - // - The `input` that kicked off this task. - // - Each `ResponseItem` that was recorded in the previous turn. - // - Each response to a `ResponseItem` (in practice, the only - // response type we seem to have is `FunctionCallOutput`). - // - // The only thing the `transcript` does not contain is the - // `pending_input` that was injected while the model was - // running. We need to add that to the conversation history - // so that the model can see it in the next turn. - [transcript.contents(), pending_input].concat() - } else { - // In practice, net_new_turn_input should contain only: - // - User messages - // - Outputs for function calls requested by the model - net_new_turn_input.extend(pending_input); - - // Responses API path – we can just send the new items and - // record the same. - net_new_turn_input - }; + [sess.state.lock().unwrap().history.contents(), pending_input].concat(); let turn_input_messages: Vec = turn_input .iter() @@ -997,8 +936,19 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { }, ); } - (ResponseItem::Reasoning { .. }, None) => { - // Omit from conversation history. + ( + ResponseItem::Reasoning { + id, + summary, + encrypted_content, + }, + None, + ) => { + items_to_record_in_conversation_history.push(ResponseItem::Reasoning { + id: id.clone(), + summary: summary.clone(), + encrypted_content: encrypted_content.clone(), + }); } _ => { warn!("Unexpected response item: {item:?} with response: {response:?}"); @@ -1027,8 +977,6 @@ async fn run_task(sess: Arc, sub_id: String, input: Vec) { }); break; } - - input_for_next_turn = responses; } Err(e) => { info!("Turn error: {e:#}"); @@ -1056,26 +1004,11 @@ async fn run_turn( sub_id: String, input: Vec, ) -> CodexResult> { - // Decide whether to use server-side storage (previous_response_id) or disable it - let (prev_id, store) = { - let state = sess.state.lock().unwrap(); - let store = state.zdr_transcript.is_none(); - let prev_id = if store { - state.previous_response_id.clone() - } else { - // When using ZDR, the Responses API may send previous_response_id - // back, but trying to use it results in a 400. - None - }; - (prev_id, store) - }; - let extra_tools = sess.mcp_connection_manager.list_all_tools(); let prompt = Prompt { input, - prev_id, user_instructions: sess.user_instructions.clone(), - store, + store: !sess.disable_response_storage, extra_tools, base_instructions_override: sess.base_instructions.clone(), }; @@ -1149,11 +1082,17 @@ async fn try_run_turn( // This usually happens because the user interrupted the model before we responded to one of its tool calls // and then the user sent a follow-up message. let missing_calls = { - sess.state - .lock() - .unwrap() - .pending_call_ids + prompt + .input .iter() + .filter_map(|ri| match ri { + ResponseItem::FunctionCall { call_id, .. } => Some(call_id), + ResponseItem::LocalShellCall { + call_id: Some(call_id), + .. + } => Some(call_id), + _ => None, + }) .filter_map(|call_id| { if completed_call_ids.contains(&call_id) { None @@ -1207,31 +1146,14 @@ async fn try_run_turn( }; match event { - ResponseEvent::Created => { - let mut state = sess.state.lock().unwrap(); - // We successfully created a new response and ensured that all pending calls were included so we can clear the pending call ids. - state.pending_call_ids.clear(); - } + ResponseEvent::Created => {} ResponseEvent::OutputItemDone(item) => { - let call_id = match &item { - ResponseItem::LocalShellCall { - call_id: Some(call_id), - .. - } => Some(call_id), - ResponseItem::FunctionCall { call_id, .. } => Some(call_id), - _ => None, - }; - if let Some(call_id) = call_id { - // We just got a new call id so we need to make sure to respond to it in the next turn. - let mut state = sess.state.lock().unwrap(); - state.pending_call_ids.insert(call_id.clone()); - } let response = handle_response_item(sess, sub_id, item.clone()).await?; output.push(ProcessedResponseItem { item, response }); } ResponseEvent::Completed { - response_id, + response_id: _, token_usage, } => { if let Some(token_usage) = token_usage { @@ -1244,8 +1166,6 @@ async fn try_run_turn( .ok(); } - let mut state = sess.state.lock().unwrap(); - state.previous_response_id = Some(response_id); return Ok(output); } ResponseEvent::OutputTextDelta(delta) => { @@ -1285,7 +1205,7 @@ async fn handle_response_item( } None } - ResponseItem::Reasoning { id: _, summary } => { + ResponseItem::Reasoning { summary, .. } => { for item in summary { let text = match item { ReasoningItemReasoningSummary::SummaryText { text } => text, @@ -1302,6 +1222,7 @@ async fn handle_response_item( name, arguments, call_id, + .. } => { info!("FunctionCall: {arguments}"); Some(handle_function_call(sess, sub_id.to_string(), name, arguments, call_id).await) @@ -2092,7 +2013,7 @@ fn format_exec_output(output: &str, exit_code: i32, duration: Duration) -> Strin fn get_last_assistant_message_from_turn(responses: &[ResponseItem]) -> Option { responses.iter().rev().find_map(|item| { - if let ResponseItem::Message { role, content } = item { + if let ResponseItem::Message { role, content, .. } = item { if role == "assistant" { content.iter().rev().find_map(|ci| { if let ContentItem::OutputText { text } = ci { @@ -2109,15 +2030,3 @@ fn get_last_assistant_message_from_turn(responses: &[ResponseItem]) -> Option bool { - if disable_response_storage { - return true; - } - - match wire_api { - WireApi::Responses => false, - WireApi::Chat => true, - } -} diff --git a/codex-rs/core/src/conversation_history.rs b/codex-rs/core/src/conversation_history.rs index 52fb1ec4f4..4cd989cbd9 100644 --- a/codex-rs/core/src/conversation_history.rs +++ b/codex-rs/core/src/conversation_history.rs @@ -1,12 +1,7 @@ use crate::models::ResponseItem; -/// Transcript of conversation history that is needed: -/// - for ZDR clients for which previous_response_id is not available, so we -/// must include the transcript with every API call. This must include each -/// `function_call` and its corresponding `function_call_output`. -/// - for clients using the "chat completions" API as opposed to the -/// "responses" API. -#[derive(Debug, Clone)] +/// Transcript of conversation history +#[derive(Debug, Clone, Default)] pub(crate) struct ConversationHistory { /// The oldest items are at the beginning of the vector. items: Vec, @@ -44,7 +39,8 @@ fn is_api_message(message: &ResponseItem) -> bool { ResponseItem::Message { role, .. } => role.as_str() != "system", ResponseItem::FunctionCallOutput { .. } | ResponseItem::FunctionCall { .. } - | ResponseItem::LocalShellCall { .. } => true, - ResponseItem::Reasoning { .. } | ResponseItem::Other => false, + | ResponseItem::LocalShellCall { .. } + | ResponseItem::Reasoning { .. } => true, + ResponseItem::Other => false, } } diff --git a/codex-rs/core/src/models.rs b/codex-rs/core/src/models.rs index 6b392fb19d..166404915a 100644 --- a/codex-rs/core/src/models.rs +++ b/codex-rs/core/src/models.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use base64::Engine; use mcp_types::CallToolResult; use serde::Deserialize; +use serde::Deserializer; use serde::Serialize; use serde::ser::Serializer; @@ -37,12 +38,14 @@ pub enum ContentItem { #[serde(tag = "type", rename_all = "snake_case")] pub enum ResponseItem { Message { + id: Option, role: String, content: Vec, }, Reasoning { id: String, summary: Vec, + encrypted_content: Option, }, LocalShellCall { /// Set when using the chat completions API. @@ -53,6 +56,7 @@ pub enum ResponseItem { action: LocalShellAction, }, FunctionCall { + id: Option, name: String, // The Responses API returns the function call arguments as a *string* that contains // JSON, not as an already‑parsed object. We keep it as a raw string here and let @@ -78,7 +82,11 @@ pub enum ResponseItem { impl From for ResponseItem { fn from(item: ResponseInputItem) -> Self { match item { - ResponseInputItem::Message { role, content } => Self::Message { role, content }, + ResponseInputItem::Message { role, content } => Self::Message { + role, + content, + id: None, + }, ResponseInputItem::FunctionCallOutput { call_id, output } => { Self::FunctionCallOutput { call_id, output } } @@ -177,7 +185,7 @@ pub struct ShellToolCallParams { pub timeout_ms: Option, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Debug, Clone)] pub struct FunctionCallOutputPayload { pub content: String, #[expect(dead_code)] @@ -205,6 +213,19 @@ impl Serialize for FunctionCallOutputPayload { } } +impl<'de> Deserialize<'de> for FunctionCallOutputPayload { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Ok(FunctionCallOutputPayload { + content: s, + success: None, + }) + } +} + // Implement Display so callers can treat the payload like a plain string when logging or doing // trivial substring checks in tests (existing tests call `.contains()` on the output). Display // returns the raw `content` field. diff --git a/codex-rs/core/src/rollout.rs b/codex-rs/core/src/rollout.rs index bb2abe45cd..0b19d13397 100644 --- a/codex-rs/core/src/rollout.rs +++ b/codex-rs/core/src/rollout.rs @@ -15,6 +15,7 @@ use tokio::io::AsyncWriteExt; use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::{self}; use tracing::info; +use tracing::warn; use uuid::Uuid; use crate::config::Config; @@ -30,9 +31,7 @@ pub struct SessionMeta { } #[derive(Serialize, Deserialize, Default, Clone)] -pub struct SessionStateSnapshot { - pub previous_response_id: Option, -} +pub struct SessionStateSnapshot {} #[derive(Serialize, Deserialize, Default, Clone)] pub struct SavedSession { @@ -119,8 +118,9 @@ impl RolloutRecorder { ResponseItem::Message { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } - | ResponseItem::FunctionCallOutput { .. } => filtered.push(item.clone()), - ResponseItem::Reasoning { .. } | ResponseItem::Other => { + | ResponseItem::FunctionCallOutput { .. } + | ResponseItem::Reasoning { .. } => filtered.push(item.clone()), + ResponseItem::Other => { // These should never be serialized. continue; } @@ -172,13 +172,17 @@ impl RolloutRecorder { } continue; } - if let Ok(item) = serde_json::from_value::(v.clone()) { - match item { + match serde_json::from_value::(v.clone()) { + Ok(item) => match item { ResponseItem::Message { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } - | ResponseItem::FunctionCallOutput { .. } => items.push(item), - ResponseItem::Reasoning { .. } | ResponseItem::Other => {} + | ResponseItem::FunctionCallOutput { .. } + | ResponseItem::Reasoning { .. } => items.push(item), + ResponseItem::Other => {} + }, + Err(e) => { + warn!("failed to parse item: {v:?}, error: {e}"); } } } @@ -267,13 +271,14 @@ async fn rollout_writer( ResponseItem::Message { .. } | ResponseItem::LocalShellCall { .. } | ResponseItem::FunctionCall { .. } - | ResponseItem::FunctionCallOutput { .. } => { + | ResponseItem::FunctionCallOutput { .. } + | ResponseItem::Reasoning { .. } => { if let Ok(json) = serde_json::to_string(&item) { let _ = file.write_all(json.as_bytes()).await; let _ = file.write_all(b"\n").await; } } - ResponseItem::Reasoning { .. } | ResponseItem::Other => {} + ResponseItem::Other => {} } } let _ = file.flush().await; diff --git a/codex-rs/core/tests/previous_response_id.rs b/codex-rs/core/tests/previous_response_id.rs deleted file mode 100644 index 6523c76441..0000000000 --- a/codex-rs/core/tests/previous_response_id.rs +++ /dev/null @@ -1,165 +0,0 @@ -use std::time::Duration; - -use codex_core::Codex; -use codex_core::ModelProviderInfo; -use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; -use codex_core::protocol::ErrorEvent; -use codex_core::protocol::EventMsg; -use codex_core::protocol::InputItem; -use codex_core::protocol::Op; -mod test_support; -use serde_json::Value; -use tempfile::TempDir; -use test_support::load_default_config_for_test; -use test_support::load_sse_fixture_with_id; -use tokio::time::timeout; -use wiremock::Match; -use wiremock::Mock; -use wiremock::MockServer; -use wiremock::Request; -use wiremock::ResponseTemplate; -use wiremock::matchers::method; -use wiremock::matchers::path; - -/// Matcher asserting that JSON body has NO `previous_response_id` field. -struct NoPrevId; - -impl Match for NoPrevId { - fn matches(&self, req: &Request) -> bool { - serde_json::from_slice::(&req.body) - .map(|v| v.get("previous_response_id").is_none()) - .unwrap_or(false) - } -} - -/// Matcher asserting that JSON body HAS a `previous_response_id` field. -struct HasPrevId; - -impl Match for HasPrevId { - fn matches(&self, req: &Request) -> bool { - serde_json::from_slice::(&req.body) - .map(|v| v.get("previous_response_id").is_some()) - .unwrap_or(false) - } -} - -/// Build minimal SSE stream with completed marker using the JSON fixture. -fn sse_completed(id: &str) -> String { - load_sse_fixture_with_id("tests/fixtures/completed_template.json", id) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn keeps_previous_response_id_between_tasks() { - #![allow(clippy::unwrap_used)] - - if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() { - println!( - "Skipping test because it cannot execute when network is disabled in a Codex sandbox." - ); - return; - } - - // Mock server - let server = MockServer::start().await; - - // First request – must NOT include `previous_response_id`. - let first = ResponseTemplate::new(200) - .insert_header("content-type", "text/event-stream") - .set_body_raw(sse_completed("resp1"), "text/event-stream"); - - Mock::given(method("POST")) - .and(path("/v1/responses")) - .and(NoPrevId) - .respond_with(first) - .expect(1) - .mount(&server) - .await; - - // Second request – MUST include `previous_response_id`. - let second = ResponseTemplate::new(200) - .insert_header("content-type", "text/event-stream") - .set_body_raw(sse_completed("resp2"), "text/event-stream"); - - Mock::given(method("POST")) - .and(path("/v1/responses")) - .and(HasPrevId) - .respond_with(second) - .expect(1) - .mount(&server) - .await; - - // Configure retry behavior explicitly to avoid mutating process-wide - // environment variables. - let model_provider = ModelProviderInfo { - name: "openai".into(), - base_url: format!("{}/v1", server.uri()), - // Environment variable that should exist in the test environment. - // ModelClient will return an error if the environment variable for the - // provider is not set. - env_key: Some("PATH".into()), - env_key_instructions: None, - wire_api: codex_core::WireApi::Responses, - query_params: None, - http_headers: None, - env_http_headers: None, - // disable retries so we don't get duplicate calls in this test - request_max_retries: Some(0), - stream_max_retries: Some(0), - stream_idle_timeout_ms: None, - }; - - // Init session - let codex_home = TempDir::new().unwrap(); - let mut config = load_default_config_for_test(&codex_home); - config.model_provider = model_provider; - let ctrl_c = std::sync::Arc::new(tokio::sync::Notify::new()); - let (codex, _init_id, _session_id) = Codex::spawn(config, ctrl_c.clone()).await.unwrap(); - - // Task 1 – triggers first request (no previous_response_id) - codex - .submit(Op::UserInput { - items: vec![InputItem::Text { - text: "hello".into(), - }], - }) - .await - .unwrap(); - - // Wait for TaskComplete - loop { - let ev = timeout(Duration::from_secs(1), codex.next_event()) - .await - .unwrap() - .unwrap(); - if matches!(ev.msg, EventMsg::TaskComplete(_)) { - break; - } - } - - // Task 2 – should include `previous_response_id` (triggers second request) - codex - .submit(Op::UserInput { - items: vec![InputItem::Text { - text: "again".into(), - }], - }) - .await - .unwrap(); - - // Wait for TaskComplete or error - loop { - let ev = timeout(Duration::from_secs(1), codex.next_event()) - .await - .unwrap() - .unwrap(); - match ev.msg { - EventMsg::TaskComplete(_) => break, - EventMsg::Error(ErrorEvent { message }) => { - panic!("unexpected error: {message}") - } - _ => { - // Ignore other events. - } - } - } -} From bc944e77f53c4ad7ae4d9df93de967d4c30d704e Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 23 Jul 2025 11:43:53 -0700 Subject: [PATCH 03/14] Improve messages emitted for exec failures (#1659) 1. Emit call_id to exec approval elicitations for mcp client convenience 2. Remove the `-retry` from the call id for the same reason as above but upstream the reset behavior to the mcp client --- codex-rs/core/src/codex.rs | 18 ++++++-------- codex-rs/core/src/protocol.rs | 2 ++ codex-rs/mcp-server/src/codex_tool_runner.rs | 1 + codex-rs/tui/src/chatwidget.rs | 3 ++- .../tui/src/conversation_history_widget.rs | 24 +++++++++++++++++++ 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index ef2cc63bbb..9dd602e237 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -253,6 +253,7 @@ impl Session { pub async fn request_command_approval( &self, sub_id: String, + call_id: String, command: Vec, cwd: PathBuf, reason: Option, @@ -261,6 +262,7 @@ impl Session { let event = Event { id: sub_id.clone(), msg: EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + call_id, command, cwd, reason, @@ -1393,6 +1395,7 @@ async fn handle_container_exec_with_params( let rx_approve = sess .request_command_approval( sub_id.clone(), + call_id.clone(), params.command.clone(), params.cwd.clone(), None, @@ -1520,6 +1523,7 @@ async fn handle_sandbox_error( let rx_approve = sess .request_command_approval( sub_id.clone(), + call_id.clone(), params.command.clone(), params.cwd.clone(), Some("command failed; retry without sandbox?".to_string()), @@ -1537,9 +1541,7 @@ async fn handle_sandbox_error( sess.notify_background_event(&sub_id, "retrying command without sandbox") .await; - // Emit a fresh Begin event so progress bars reset. - let retry_call_id = format!("{call_id}-retry"); - sess.notify_exec_command_begin(&sub_id, &retry_call_id, ¶ms) + sess.notify_exec_command_begin(&sub_id, &call_id, ¶ms) .await; // This is an escalated retry; the policy will not be @@ -1562,14 +1564,8 @@ async fn handle_sandbox_error( duration, } = retry_output; - sess.notify_exec_command_end( - &sub_id, - &retry_call_id, - &stdout, - &stderr, - exit_code, - ) - .await; + sess.notify_exec_command_end(&sub_id, &call_id, &stdout, &stderr, exit_code) + .await; let is_success = exit_code == 0; let content = format_exec_output( diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 9f6e004b67..943c0b928f 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -422,6 +422,8 @@ pub struct ExecCommandEndEvent { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ExecApprovalRequestEvent { + /// Identifier for the associated exec call, if available. + pub call_id: String, /// The command to be executed. pub command: Vec, /// The command's working directory. diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 9aaab54395..2b7b08f94c 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -156,6 +156,7 @@ async fn run_codex_tool_session_inner( EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { command, cwd, + call_id: _, reason: _, }) => { handle_exec_approval_request( diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c70c6f6d72..51f4df5273 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -314,6 +314,7 @@ impl ChatWidget<'_> { self.bottom_pane.set_task_running(false); } EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { + call_id: _, command, cwd, reason, @@ -362,7 +363,7 @@ impl ChatWidget<'_> { cwd: _, }) => { self.conversation_history - .add_active_exec_command(call_id, command); + .reset_or_add_active_exec_command(call_id, command); self.request_redraw(); } EventMsg::PatchApplyBegin(PatchApplyBeginEvent { diff --git a/codex-rs/tui/src/conversation_history_widget.rs b/codex-rs/tui/src/conversation_history_widget.rs index 01a8dc6834..ceaf115f33 100644 --- a/codex-rs/tui/src/conversation_history_widget.rs +++ b/codex-rs/tui/src/conversation_history_widget.rs @@ -235,6 +235,30 @@ impl ConversationHistoryWidget { self.add_to_history(HistoryCell::new_active_exec_command(call_id, command)); } + /// If an ActiveExecCommand with the same call_id already exists, replace + /// it with a fresh one (resetting start time and view). Otherwise, add a new entry. + pub fn reset_or_add_active_exec_command(&mut self, call_id: String, command: Vec) { + // Find the most recent matching ActiveExecCommand. + let maybe_idx = self.entries.iter().rposition(|entry| { + if let HistoryCell::ActiveExecCommand { call_id: id, .. } = &entry.cell { + id == &call_id + } else { + false + } + }); + + if let Some(idx) = maybe_idx { + let width = self.cached_width.get(); + self.entries[idx].cell = HistoryCell::new_active_exec_command(call_id.clone(), command); + if width > 0 { + let height = self.entries[idx].cell.height(width); + self.entries[idx].line_count.set(height); + } + } else { + self.add_active_exec_command(call_id, command); + } + } + pub fn add_active_mcp_tool_call( &mut self, call_id: String, From 084236f7175aa8a8024661845acb3e11e96f7c0a Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 23 Jul 2025 12:55:35 -0700 Subject: [PATCH 04/14] Add call_id to patch approvals and elicitations (#1660) Builds on https://github.com/openai/codex/pull/1659 and adds call_id to a few more places for the same reason. --- codex-rs/core/src/codex.rs | 13 +++++++++++-- codex-rs/core/src/protocol.rs | 2 ++ codex-rs/mcp-server/src/codex_tool_runner.rs | 5 ++++- codex-rs/mcp-server/src/exec_approval.rs | 4 ++++ codex-rs/mcp-server/src/patch_approval.rs | 3 +++ codex-rs/mcp-server/tests/codex_tool.rs | 2 ++ codex-rs/tui/src/chatwidget.rs | 1 + 7 files changed, 27 insertions(+), 3 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 9dd602e237..73bb714971 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -279,6 +279,7 @@ impl Session { pub async fn request_patch_approval( &self, sub_id: String, + call_id: String, action: &ApplyPatchAction, reason: Option, grant_root: Option, @@ -287,6 +288,7 @@ impl Session { let event = Event { id: sub_id.clone(), msg: EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + call_id, changes: convert_apply_patch_to_protocol(action), reason, grant_root, @@ -1629,7 +1631,7 @@ async fn apply_patch( // Compute a readable summary of path changes to include in the // approval request so the user can make an informed decision. let rx_approve = sess - .request_patch_approval(sub_id.clone(), &action, None, None) + .request_patch_approval(sub_id.clone(), call_id.clone(), &action, None, None) .await; match rx_approve.await.unwrap_or_default() { ReviewDecision::Approved | ReviewDecision::ApprovedForSession => false, @@ -1667,7 +1669,13 @@ async fn apply_patch( )); let rx = sess - .request_patch_approval(sub_id.clone(), &action, reason.clone(), Some(root.clone())) + .request_patch_approval( + sub_id.clone(), + call_id.clone(), + &action, + reason.clone(), + Some(root.clone()), + ) .await; if !matches!( @@ -1751,6 +1759,7 @@ async fn apply_patch( let rx = sess .request_patch_approval( sub_id.clone(), + call_id.clone(), &action, reason.clone(), Some(root.clone()), diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index 943c0b928f..cc201bc7ea 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -435,6 +435,8 @@ pub struct ExecApprovalRequestEvent { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ApplyPatchApprovalRequestEvent { + /// Responses API call id for the associated patch apply call, if available. + pub call_id: String, pub changes: HashMap, /// Optional explanatory reason (e.g. request for extra write access). #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 2b7b08f94c..4e10d158cf 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -156,7 +156,7 @@ async fn run_codex_tool_session_inner( EventMsg::ExecApprovalRequest(ExecApprovalRequestEvent { command, cwd, - call_id: _, + call_id, reason: _, }) => { handle_exec_approval_request( @@ -167,6 +167,7 @@ async fn run_codex_tool_session_inner( request_id.clone(), request_id_str.clone(), event.id.clone(), + call_id, ) .await; continue; @@ -180,11 +181,13 @@ async fn run_codex_tool_session_inner( break; } EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + call_id, reason, grant_root, changes, }) => { handle_patch_approval_request( + call_id, reason, grant_root, changes, diff --git a/codex-rs/mcp-server/src/exec_approval.rs b/codex-rs/mcp-server/src/exec_approval.rs index fc0c41d0d1..f073214bf5 100644 --- a/codex-rs/mcp-server/src/exec_approval.rs +++ b/codex-rs/mcp-server/src/exec_approval.rs @@ -32,6 +32,7 @@ pub struct ExecApprovalElicitRequestParams { pub codex_elicitation: String, pub codex_mcp_tool_call_id: String, pub codex_event_id: String, + pub codex_call_id: String, pub codex_command: Vec, pub codex_cwd: PathBuf, } @@ -45,6 +46,7 @@ pub struct ExecApprovalResponse { pub decision: ReviewDecision, } +#[allow(clippy::too_many_arguments)] pub(crate) async fn handle_exec_approval_request( command: Vec, cwd: PathBuf, @@ -53,6 +55,7 @@ pub(crate) async fn handle_exec_approval_request( request_id: RequestId, tool_call_id: String, event_id: String, + call_id: String, ) { let escaped_command = shlex::try_join(command.iter().map(|s| s.as_str())).unwrap_or_else(|_| command.join(" ")); @@ -71,6 +74,7 @@ pub(crate) async fn handle_exec_approval_request( codex_elicitation: "exec-approval".to_string(), codex_mcp_tool_call_id: tool_call_id.clone(), codex_event_id: event_id.clone(), + codex_call_id: call_id, codex_command: command, codex_cwd: cwd, }; diff --git a/codex-rs/mcp-server/src/patch_approval.rs b/codex-rs/mcp-server/src/patch_approval.rs index bfccfa50ee..db99ee5f27 100644 --- a/codex-rs/mcp-server/src/patch_approval.rs +++ b/codex-rs/mcp-server/src/patch_approval.rs @@ -27,6 +27,7 @@ pub struct PatchApprovalElicitRequestParams { pub codex_elicitation: String, pub codex_mcp_tool_call_id: String, pub codex_event_id: String, + pub codex_call_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub codex_reason: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -41,6 +42,7 @@ pub struct PatchApprovalResponse { #[allow(clippy::too_many_arguments)] pub(crate) async fn handle_patch_approval_request( + call_id: String, reason: Option, grant_root: Option, changes: HashMap, @@ -66,6 +68,7 @@ pub(crate) async fn handle_patch_approval_request( codex_elicitation: "patch-approval".to_string(), codex_mcp_tool_call_id: tool_call_id.clone(), codex_event_id: event_id.clone(), + codex_call_id: call_id, codex_reason: reason, codex_grant_root: grant_root, codex_changes: changes, diff --git a/codex-rs/mcp-server/tests/codex_tool.rs b/codex-rs/mcp-server/tests/codex_tool.rs index d36813ce9f..0e31eea46e 100644 --- a/codex-rs/mcp-server/tests/codex_tool.rs +++ b/codex-rs/mcp-server/tests/codex_tool.rs @@ -171,6 +171,7 @@ fn create_expected_elicitation_request( codex_event_id, codex_command: command, codex_cwd: workdir.to_path_buf(), + codex_call_id: "call1234".to_string(), })?), }) } @@ -384,6 +385,7 @@ fn create_expected_patch_approval_elicitation_request( codex_reason: reason, codex_grant_root: grant_root, codex_changes: changes, + codex_call_id: "call1234".to_string(), })?), }) } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 51f4df5273..3856587010 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -328,6 +328,7 @@ impl ChatWidget<'_> { self.bottom_pane.push_approval_request(request); } EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent { + call_id: _, changes, reason, grant_root, From b4ab7c1b7325fef16fb2c15f78d9b41dd1d3485b Mon Sep 17 00:00:00 2001 From: aibrahim-oai Date: Wed, 23 Jul 2025 15:03:26 -0700 Subject: [PATCH 05/14] Flaky CI fix (#1647) Flushing before sending `TaskCompleteEvent` and ending the submission loop to avoid race conditions. --- codex-rs/core/src/codex.rs | 31 +++++++++++++ codex-rs/core/src/protocol.rs | 6 +++ codex-rs/core/src/rollout.rs | 21 ++++++++- codex-rs/exec/src/event_processor.rs | 35 ++++++++++++++- .../src/event_processor_with_human_output.rs | 28 ++++++++++-- .../src/event_processor_with_json_output.rs | 24 ++++++++-- codex-rs/exec/src/lib.rs | 44 +++++-------------- codex-rs/mcp-server/src/codex_tool_runner.rs | 3 +- codex-rs/tui/src/app.rs | 4 +- codex-rs/tui/src/chatwidget.rs | 4 ++ 10 files changed, 153 insertions(+), 47 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index 73bb714971..4cc888b62e 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -812,6 +812,37 @@ async fn submission_loop( } }); } + Op::Shutdown => { + info!("Shutting down Codex instance"); + + // Gracefully flush and shutdown rollout recorder on session end so tests + // that inspect the rollout file do not race with the background writer. + if let Some(sess_arc) = sess { + let recorder_opt = sess_arc.rollout.lock().unwrap().take(); + if let Some(rec) = recorder_opt { + if let Err(e) = rec.shutdown().await { + warn!("failed to shutdown rollout recorder: {e}"); + let event = Event { + id: sub.id.clone(), + msg: EventMsg::Error(ErrorEvent { + message: "Failed to shutdown rollout recorder".to_string(), + }), + }; + if let Err(e) = tx_event.send(event).await { + warn!("failed to send error message: {e:?}"); + } + } + } + } + let event = Event { + id: sub.id.clone(), + msg: EventMsg::ShutdownComplete, + }; + if let Err(e) = tx_event.send(event).await { + warn!("failed to send Shutdown event: {e}"); + } + break; + } } } debug!("Agent loop exited"); diff --git a/codex-rs/core/src/protocol.rs b/codex-rs/core/src/protocol.rs index cc201bc7ea..0c375e455d 100644 --- a/codex-rs/core/src/protocol.rs +++ b/codex-rs/core/src/protocol.rs @@ -116,6 +116,9 @@ pub enum Op { /// Request a single history entry identified by `log_id` + `offset`. GetHistoryEntryRequest { offset: usize, log_id: u64 }, + + /// Request to shut down codex instance. + Shutdown, } /// Determines the conditions under which the user is consulted to approve @@ -326,6 +329,9 @@ pub enum EventMsg { /// Response to GetHistoryEntryRequest. GetHistoryEntryResponse(GetHistoryEntryResponseEvent), + + /// Notification that the agent is shutting down. + ShutdownComplete, } // Individual event payload types matching each `EventMsg` variant. diff --git a/codex-rs/core/src/rollout.rs b/codex-rs/core/src/rollout.rs index 0b19d13397..7f0f61b9eb 100644 --- a/codex-rs/core/src/rollout.rs +++ b/codex-rs/core/src/rollout.rs @@ -14,6 +14,7 @@ use time::macros::format_description; use tokio::io::AsyncWriteExt; use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::{self}; +use tokio::sync::oneshot; use tracing::info; use tracing::warn; use uuid::Uuid; @@ -57,10 +58,10 @@ pub(crate) struct RolloutRecorder { tx: Sender, } -#[derive(Clone)] enum RolloutCmd { AddItems(Vec), UpdateState(SessionStateSnapshot), + Shutdown { ack: oneshot::Sender<()> }, } impl RolloutRecorder { @@ -204,6 +205,21 @@ impl RolloutRecorder { info!("Resumed rollout successfully from {path:?}"); Ok((Self { tx }, saved)) } + + pub async fn shutdown(&self) -> std::io::Result<()> { + let (tx_done, rx_done) = oneshot::channel(); + match self.tx.send(RolloutCmd::Shutdown { ack: tx_done }).await { + Ok(_) => rx_done + .await + .map_err(|e| IoError::other(format!("failed waiting for rollout shutdown: {e}"))), + Err(e) => { + warn!("failed to send rollout shutdown command: {e}"); + Err(IoError::other(format!( + "failed to send rollout shutdown command: {e}" + ))) + } + } + } } struct LogFileInfo { @@ -299,6 +315,9 @@ async fn rollout_writer( let _ = file.flush().await; } } + RolloutCmd::Shutdown { ack } => { + let _ = ack.send(()); + } } } } diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 56db651a83..a7edb96af2 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -1,15 +1,23 @@ +use std::path::Path; + use codex_common::summarize_sandbox_policy; use codex_core::WireApi; use codex_core::config::Config; use codex_core::model_supports_reasoning_summaries; use codex_core::protocol::Event; +pub(crate) enum CodexStatus { + Running, + InitiateShutdown, + Shutdown, +} + pub(crate) trait EventProcessor { /// Print summary of effective configuration and user prompt. fn print_config_summary(&mut self, config: &Config, prompt: &str); /// Handle a single event emitted by the agent. - fn process_event(&mut self, event: Event); + fn process_event(&mut self, event: Event) -> CodexStatus; } pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static str, String)> { @@ -35,3 +43,28 @@ pub(crate) fn create_config_summary_entries(config: &Config) -> Vec<(&'static st entries } + +pub(crate) fn handle_last_message( + last_agent_message: Option<&str>, + last_message_path: Option<&Path>, +) { + match (last_message_path, last_agent_message) { + (Some(path), Some(msg)) => write_last_message_file(msg, Some(path)), + (Some(path), None) => { + write_last_message_file("", Some(path)); + eprintln!( + "Warning: no last agent message; wrote empty content to {}", + path.display() + ); + } + (None, _) => eprintln!("Warning: no file to write last message to."), + } +} + +fn write_last_message_file(contents: &str, last_message_path: Option<&Path>) { + if let Some(path) = last_message_path { + if let Err(e) = std::fs::write(path, contents) { + eprintln!("Failed to write last message file {path:?}: {e}"); + } + } +} diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 7b39071116..bc647c683e 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -15,16 +15,20 @@ use codex_core::protocol::McpToolCallEndEvent; use codex_core::protocol::PatchApplyBeginEvent; use codex_core::protocol::PatchApplyEndEvent; use codex_core::protocol::SessionConfiguredEvent; +use codex_core::protocol::TaskCompleteEvent; use codex_core::protocol::TokenUsage; use owo_colors::OwoColorize; use owo_colors::Style; use shlex::try_join; use std::collections::HashMap; use std::io::Write; +use std::path::PathBuf; use std::time::Instant; +use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; use crate::event_processor::create_config_summary_entries; +use crate::event_processor::handle_last_message; /// This should be configurable. When used in CI, users may not want to impose /// a limit so they can see the full transcript. @@ -54,10 +58,15 @@ pub(crate) struct EventProcessorWithHumanOutput { show_agent_reasoning: bool, answer_started: bool, reasoning_started: bool, + last_message_path: Option, } impl EventProcessorWithHumanOutput { - pub(crate) fn create_with_ansi(with_ansi: bool, config: &Config) -> Self { + pub(crate) fn create_with_ansi( + with_ansi: bool, + config: &Config, + last_message_path: Option, + ) -> Self { let call_id_to_command = HashMap::new(); let call_id_to_patch = HashMap::new(); let call_id_to_tool_call = HashMap::new(); @@ -77,6 +86,7 @@ impl EventProcessorWithHumanOutput { show_agent_reasoning: !config.hide_agent_reasoning, answer_started: false, reasoning_started: false, + last_message_path, } } else { Self { @@ -93,6 +103,7 @@ impl EventProcessorWithHumanOutput { show_agent_reasoning: !config.hide_agent_reasoning, answer_started: false, reasoning_started: false, + last_message_path, } } } @@ -158,7 +169,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { ); } - fn process_event(&mut self, event: Event) { + fn process_event(&mut self, event: Event) -> CodexStatus { let Event { id: _, msg } = event; match msg { EventMsg::Error(ErrorEvent { message }) => { @@ -168,9 +179,16 @@ impl EventProcessor for EventProcessorWithHumanOutput { EventMsg::BackgroundEvent(BackgroundEventEvent { message }) => { ts_println!(self, "{}", message.style(self.dimmed)); } - EventMsg::TaskStarted | EventMsg::TaskComplete(_) => { + EventMsg::TaskStarted => { // Ignore. } + EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { + handle_last_message( + last_agent_message.as_deref(), + self.last_message_path.as_deref(), + ); + return CodexStatus::InitiateShutdown; + } EventMsg::TokenCount(TokenUsage { total_tokens, .. }) => { ts_println!(self, "tokens used: {total_tokens}"); } @@ -185,7 +203,7 @@ impl EventProcessor for EventProcessorWithHumanOutput { } EventMsg::AgentReasoningDelta(AgentReasoningDeltaEvent { delta }) => { if !self.show_agent_reasoning { - return; + return CodexStatus::Running; } if !self.reasoning_started { ts_println!( @@ -498,7 +516,9 @@ impl EventProcessor for EventProcessorWithHumanOutput { EventMsg::GetHistoryEntryResponse(_) => { // Currently ignored in exec output. } + EventMsg::ShutdownComplete => return CodexStatus::Shutdown, } + CodexStatus::Running } } diff --git a/codex-rs/exec/src/event_processor_with_json_output.rs b/codex-rs/exec/src/event_processor_with_json_output.rs index 699460bbed..e7a658b76f 100644 --- a/codex-rs/exec/src/event_processor_with_json_output.rs +++ b/codex-rs/exec/src/event_processor_with_json_output.rs @@ -1,18 +1,24 @@ use std::collections::HashMap; +use std::path::PathBuf; use codex_core::config::Config; use codex_core::protocol::Event; use codex_core::protocol::EventMsg; +use codex_core::protocol::TaskCompleteEvent; use serde_json::json; +use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; use crate::event_processor::create_config_summary_entries; +use crate::event_processor::handle_last_message; -pub(crate) struct EventProcessorWithJsonOutput; +pub(crate) struct EventProcessorWithJsonOutput { + last_message_path: Option, +} impl EventProcessorWithJsonOutput { - pub fn new() -> Self { - Self {} + pub fn new(last_message_path: Option) -> Self { + Self { last_message_path } } } @@ -33,15 +39,25 @@ impl EventProcessor for EventProcessorWithJsonOutput { println!("{prompt_json}"); } - fn process_event(&mut self, event: Event) { + fn process_event(&mut self, event: Event) -> CodexStatus { match event.msg { EventMsg::AgentMessageDelta(_) | EventMsg::AgentReasoningDelta(_) => { // Suppress streaming events in JSON mode. + CodexStatus::Running } + EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { + handle_last_message( + last_agent_message.as_deref(), + self.last_message_path.as_deref(), + ); + CodexStatus::InitiateShutdown + } + EventMsg::ShutdownComplete => CodexStatus::Shutdown, _ => { if let Ok(line) = serde_json::to_string(&event) { println!("{line}"); } + CodexStatus::Running } } } diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 620ab82327..126e92f597 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -5,7 +5,6 @@ mod event_processor_with_json_output; use std::io::IsTerminal; use std::io::Read; -use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -28,6 +27,7 @@ use tracing::error; use tracing::info; use tracing_subscriber::EnvFilter; +use crate::event_processor::CodexStatus; use crate::event_processor::EventProcessor; pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> anyhow::Result<()> { @@ -123,11 +123,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?; let mut event_processor: Box = if json_mode { - Box::new(EventProcessorWithJsonOutput::new()) + Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone())) } else { Box::new(EventProcessorWithHumanOutput::create_with_ansi( stdout_with_ansi, &config, + last_message_file.clone(), )) }; @@ -224,40 +225,17 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any // Run the loop until the task is complete. while let Some(event) = rx.recv().await { - let (is_last_event, last_assistant_message) = match &event.msg { - EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => { - (true, last_agent_message.clone()) + let shutdown: CodexStatus = event_processor.process_event(event); + match shutdown { + CodexStatus::Running => continue, + CodexStatus::InitiateShutdown => { + codex.submit(Op::Shutdown).await?; + } + CodexStatus::Shutdown => { + break; } - _ => (false, None), - }; - event_processor.process_event(event); - if is_last_event { - handle_last_message(last_assistant_message, last_message_file.as_deref())?; - break; } } Ok(()) } - -fn handle_last_message( - last_agent_message: Option, - last_message_file: Option<&Path>, -) -> std::io::Result<()> { - match (last_agent_message, last_message_file) { - (Some(last_agent_message), Some(last_message_file)) => { - // Last message and a file to write to. - std::fs::write(last_message_file, last_agent_message)?; - } - (None, Some(last_message_file)) => { - eprintln!( - "Warning: No last message to write to file: {}", - last_message_file.to_string_lossy() - ); - } - (_, None) => { - // No last message and no file to write to. - } - } - Ok(()) -} diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 4e10d158cf..f2cacf6c8e 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -246,7 +246,8 @@ async fn run_codex_tool_session_inner( | EventMsg::BackgroundEvent(_) | EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyEnd(_) - | EventMsg::GetHistoryEntryResponse(_) => { + | EventMsg::GetHistoryEntryResponse(_) + | EventMsg::ShutdownComplete => { // For now, we do not do anything extra for these // events. Note that // send(codex_event_to_notification(&event)) above has diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 37c2616d5b..377b5d6f0b 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -223,9 +223,7 @@ impl App<'_> { } => { match &mut self.app_state { AppState::Chat { widget } => { - if widget.on_ctrl_c() { - self.app_event_tx.send(AppEvent::ExitRequest); - } + widget.on_ctrl_c(); } AppState::Login { .. } | AppState::GitWarning { .. } => { // No-op. diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 3856587010..081a406f29 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -419,6 +419,9 @@ impl ChatWidget<'_> { self.bottom_pane .on_history_entry_response(log_id, offset, entry.map(|e| e.text)); } + EventMsg::ShutdownComplete => { + self.app_event_tx.send(AppEvent::ExitRequest); + } event => { self.conversation_history .add_background_event(format!("{event:?}")); @@ -471,6 +474,7 @@ impl ChatWidget<'_> { self.reasoning_buffer.clear(); false } else if self.bottom_pane.ctrl_c_quit_hint_visible() { + self.submit_op(Op::Shutdown); true } else { self.bottom_pane.show_ctrl_c_quit_hint(); From 6dd62ffa3b6cf754f8e9a7774e036242d0f4083f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:20:47 -0700 Subject: [PATCH 06/14] chore(deps-dev): bump @types/bun from 1.2.18 to 1.2.19 in /.github/actions/codex (#1635) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/bun&package-manager=bun&previous-version=1.2.18&new-version=1.2.19)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/codex/bun.lock | 6 +++--- .github/actions/codex/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/actions/codex/bun.lock b/.github/actions/codex/bun.lock index e7382ff7a2..9360d4d1f6 100644 --- a/.github/actions/codex/bun.lock +++ b/.github/actions/codex/bun.lock @@ -8,7 +8,7 @@ "@actions/github": "^6.0.1", }, "devDependencies": { - "@types/bun": "^1.2.18", + "@types/bun": "^1.2.19", "@types/node": "^24.0.13", "prettier": "^3.6.2", "typescript": "^5.8.3", @@ -48,7 +48,7 @@ "@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], - "@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="], + "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], "@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], @@ -56,7 +56,7 @@ "before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="], - "bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="], + "bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="], "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], diff --git a/.github/actions/codex/package.json b/.github/actions/codex/package.json index 53260f4d58..ded2b7aac5 100644 --- a/.github/actions/codex/package.json +++ b/.github/actions/codex/package.json @@ -13,7 +13,7 @@ "@actions/github": "^6.0.1" }, "devDependencies": { - "@types/bun": "^1.2.18", + "@types/bun": "^1.2.19", "@types/node": "^24.0.13", "prettier": "^3.6.2", "typescript": "^5.8.3" From 4fc4e410bd34b496a33f9e94c9c1abe00ff0dd7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:32:31 -0700 Subject: [PATCH 07/14] chore(deps-dev): bump @types/node from 24.0.13 to 24.0.15 in /.github/actions/codex (#1636) [![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@types/node&package-manager=bun&previous-version=24.0.13&new-version=24.0.15)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/codex/bun.lock | 6 ++++-- .github/actions/codex/package.json | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/actions/codex/bun.lock b/.github/actions/codex/bun.lock index 9360d4d1f6..8b546a5ac6 100644 --- a/.github/actions/codex/bun.lock +++ b/.github/actions/codex/bun.lock @@ -9,7 +9,7 @@ }, "devDependencies": { "@types/bun": "^1.2.19", - "@types/node": "^24.0.13", + "@types/node": "^24.1.0", "prettier": "^3.6.2", "typescript": "^5.8.3", }, @@ -50,7 +50,7 @@ "@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="], - "@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], @@ -82,6 +82,8 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@12.6.0", "", { "dependencies": { "@octokit/openapi-types": "^20.0.0" } }, "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw=="], + "bun-types/@types/node": ["@types/node@24.0.13", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ=="], + "@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@20.0.0", "", {}, "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA=="], diff --git a/.github/actions/codex/package.json b/.github/actions/codex/package.json index ded2b7aac5..21817b8a59 100644 --- a/.github/actions/codex/package.json +++ b/.github/actions/codex/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "@types/bun": "^1.2.19", - "@types/node": "^24.0.13", + "@types/node": "^24.1.0", "prettier": "^3.6.2", "typescript": "^5.8.3" } From 6e1838e0d8bb66312479250cf3c3558a647d1501 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:36:08 -0700 Subject: [PATCH 08/14] chore(deps): bump rand from 0.9.1 to 0.9.2 in /codex-rs (#1637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [rand](https://github.com/rust-random/rand) from 0.9.1 to 0.9.2.
Changelog

Sourced from rand's changelog.

[0.9.2 — 2025-07-20]

Deprecated

  • Deprecate rand::rngs::mock module and StepRng generator (#1634)

Additions

  • Enable WeightedIndex<usize> (de)serialization (#1646)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=rand&package-manager=cargo&previous-version=0.9.1&new-version=0.9.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 3e4b84a435..e1d64a93b9 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -676,7 +676,7 @@ dependencies = [ "openssl-sys", "predicates", "pretty_assertions", - "rand 0.9.1", + "rand 0.9.2", "reqwest", "seccompiler", "serde", @@ -3279,9 +3279,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.3", From db847220801d4065e1b13f5979907db84be5cbed Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Wed, 23 Jul 2025 15:40:00 -0700 Subject: [PATCH 09/14] Fix flaky test (#1664) Co-authored-by: aibrahim-oai --- codex-rs/chatgpt/src/apply_command.rs | 24 +++++++++++++++------ codex-rs/chatgpt/tests/apply_command_e2e.rs | 14 ++---------- codex-rs/cli/src/main.rs | 2 +- 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/codex-rs/chatgpt/src/apply_command.rs b/codex-rs/chatgpt/src/apply_command.rs index 4209d958e1..52ab205a0c 100644 --- a/codex-rs/chatgpt/src/apply_command.rs +++ b/codex-rs/chatgpt/src/apply_command.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Parser; use codex_common::CliConfigOverrides; use codex_core::config::Config; @@ -17,7 +19,10 @@ pub struct ApplyCommand { #[clap(flatten)] pub config_overrides: CliConfigOverrides, } -pub async fn run_apply_command(apply_cli: ApplyCommand) -> anyhow::Result<()> { +pub async fn run_apply_command( + apply_cli: ApplyCommand, + cwd: Option, +) -> anyhow::Result<()> { let config = Config::load_with_cli_overrides( apply_cli .config_overrides @@ -29,10 +34,13 @@ pub async fn run_apply_command(apply_cli: ApplyCommand) -> anyhow::Result<()> { init_chatgpt_token_from_auth(&config.codex_home).await?; let task_response = get_task(&config, apply_cli.task_id).await?; - apply_diff_from_task(task_response).await + apply_diff_from_task(task_response, cwd).await } -pub async fn apply_diff_from_task(task_response: GetTaskResponse) -> anyhow::Result<()> { +pub async fn apply_diff_from_task( + task_response: GetTaskResponse, + cwd: Option, +) -> anyhow::Result<()> { let diff_turn = match task_response.current_diff_task_turn { Some(turn) => turn, None => anyhow::bail!("No diff turn found"), @@ -42,13 +50,17 @@ pub async fn apply_diff_from_task(task_response: GetTaskResponse) -> anyhow::Res _ => None, }); match output_diff { - Some(output_diff) => apply_diff(&output_diff.diff).await, + Some(output_diff) => apply_diff(&output_diff.diff, cwd).await, None => anyhow::bail!("No PR output item found"), } } -async fn apply_diff(diff: &str) -> anyhow::Result<()> { - let toplevel_output = tokio::process::Command::new("git") +async fn apply_diff(diff: &str, cwd: Option) -> anyhow::Result<()> { + let mut cmd = tokio::process::Command::new("git"); + if let Some(cwd) = cwd { + cmd.current_dir(cwd); + } + let toplevel_output = cmd .args(vec!["rev-parse", "--show-toplevel"]) .output() .await?; diff --git a/codex-rs/chatgpt/tests/apply_command_e2e.rs b/codex-rs/chatgpt/tests/apply_command_e2e.rs index e395e4f155..45c33bedb4 100644 --- a/codex-rs/chatgpt/tests/apply_command_e2e.rs +++ b/codex-rs/chatgpt/tests/apply_command_e2e.rs @@ -78,17 +78,7 @@ async fn test_apply_command_creates_fibonacci_file() { .await .expect("Failed to load fixture"); - let original_dir = std::env::current_dir().expect("Failed to get current dir"); - std::env::set_current_dir(repo_path).expect("Failed to change directory"); - struct DirGuard(std::path::PathBuf); - impl Drop for DirGuard { - fn drop(&mut self) { - let _ = std::env::set_current_dir(&self.0); - } - } - let _guard = DirGuard(original_dir); - - apply_diff_from_task(task_response) + apply_diff_from_task(task_response, Some(repo_path.to_path_buf())) .await .expect("Failed to apply diff from task"); @@ -173,7 +163,7 @@ console.log(fib(10)); .await .expect("Failed to load fixture"); - let apply_result = apply_diff_from_task(task_response).await; + let apply_result = apply_diff_from_task(task_response, Some(repo_path.to_path_buf())).await; assert!( apply_result.is_err(), diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 7e23782d75..e397b0ca6a 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -145,7 +145,7 @@ async fn cli_main(codex_linux_sandbox_exe: Option) -> anyhow::Result<() }, Some(Subcommand::Apply(mut apply_cli)) => { prepend_config_flags(&mut apply_cli.config_overrides, cli.config_overrides); - run_apply_command(apply_cli).await?; + run_apply_command(apply_cli, None).await?; } } From 9f645353e913014692c64b81c027b7e57d3762e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:07:33 -0700 Subject: [PATCH 10/14] chore(deps): bump strum from 0.27.1 to 0.27.2 in /codex-rs (#1639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [strum](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
Release notes

Sourced from strum's releases.

v0.27.2

What's Changed

New Contributors

Full Changelog: https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2

Changelog

Sourced from strum's changelog.

0.27.2

  • #141: Adding support for doc comments on EnumDiscriminants generated type.

    • The doc comment will be copied from the variant on the type itself.
  • #435:allow discriminants on empty enum.

  • #443: Change enum table callbacks to FnMut.

  • #444: Add #[automatically_derived] to the impls by @​dandedotdev in Peternator7/strum#444

    • This should make the linter less noisy with warnings in generated code.
  • #440: Implement a suffix attribute for serialization of enum variants.

    #[derive(strum::Display)]
    #[strum(suffix=".json")]
    #[strum(serialize_all="snake_case")]
    enum StorageConfiguration {
      PostgresProvider,
      S3StorageProvider,
      AzureStorageProvider,
    }
    

    fn main() { let response = SurveyResponse::Other("It was good".into()); println!("Loading configuration from: {}", StorageConfiguration::PostgresProvider); // prints: Loaded Configuration from: postgres_provider.json }

  • #446: Drop needless rustversion dependency.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=strum&package-manager=cargo&previous-version=0.27.1&new-version=0.27.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 6 +++--- codex-rs/tui/Cargo.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e1d64a93b9..2b7a6ec083 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -840,7 +840,7 @@ dependencies = [ "regex-lite", "serde_json", "shlex", - "strum 0.27.1", + "strum 0.27.2", "strum_macros 0.27.1", "tokio", "tracing", @@ -4263,9 +4263,9 @@ dependencies = [ [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" [[package]] name = "strum_macros" diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 74aedfa353..654efd5c9c 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -42,7 +42,7 @@ ratatui-image = "8.0.0" regex-lite = "0.1" serde_json = { version = "1", features = ["preserve_order"] } shlex = "1.3.0" -strum = "0.27.1" +strum = "0.27.2" strum_macros = "0.27.1" tokio = { version = "1", features = [ "io-std", From 4a57afaaf2647101b015cbc83775def74fe9f96f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:34:16 -0700 Subject: [PATCH 11/14] chore(deps): bump strum_macros from 0.27.1 to 0.27.2 in /codex-rs (#1638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.27.1 to 0.27.2.
Release notes

Sourced from strum_macros's releases.

v0.27.2

What's Changed

New Contributors

Full Changelog: https://github.com/Peternator7/strum/compare/v0.27.1...v0.27.2

Changelog

Sourced from strum_macros's changelog.

0.27.2

  • #141: Adding support for doc comments on EnumDiscriminants generated type.

    • The doc comment will be copied from the variant on the type itself.
  • #435:allow discriminants on empty enum.

  • #443: Change enum table callbacks to FnMut.

  • #444: Add #[automatically_derived] to the impls by @​dandedotdev in Peternator7/strum#444

    • This should make the linter less noisy with warnings in generated code.
  • #440: Implement a suffix attribute for serialization of enum variants.

    #[derive(strum::Display)]
    #[strum(suffix=".json")]
    #[strum(serialize_all="snake_case")]
    enum StorageConfiguration {
      PostgresProvider,
      S3StorageProvider,
      AzureStorageProvider,
    }
    

    fn main() { let response = SurveyResponse::Other("It was good".into()); println!("Loading configuration from: {}", StorageConfiguration::PostgresProvider); // prints: Loaded Configuration from: postgres_provider.json }

  • #446: Drop needless rustversion dependency.

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=strum_macros&package-manager=cargo&previous-version=0.27.1&new-version=0.27.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 9 ++++----- codex-rs/core/Cargo.toml | 2 +- codex-rs/tui/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2b7a6ec083..e1c17e1850 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -682,7 +682,7 @@ dependencies = [ "serde", "serde_json", "sha1", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "tempfile", "thiserror 2.0.12", "time", @@ -841,7 +841,7 @@ dependencies = [ "serde_json", "shlex", "strum 0.27.2", - "strum_macros 0.27.1", + "strum_macros 0.27.2", "tokio", "tracing", "tracing-appender", @@ -4282,14 +4282,13 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" dependencies = [ "heck", "proc-macro2", "quote", - "rustversion", "syn 2.0.104", ] diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index a87894bc4d..2d0877633a 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -30,7 +30,7 @@ reqwest = { version = "0.12", features = ["json", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sha1 = "0.10.6" -strum_macros = "0.27.1" +strum_macros = "0.27.2" thiserror = "2.0.12" time = { version = "0.3", features = ["formatting", "local-offset", "macros"] } tokio = { version = "1", features = [ diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index 654efd5c9c..b2f2b9b653 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -43,7 +43,7 @@ regex-lite = "0.1" serde_json = { version = "1", features = ["preserve_order"] } shlex = "1.3.0" strum = "0.27.2" -strum_macros = "0.27.1" +strum_macros = "0.27.2" tokio = { version = "1", features = [ "io-std", "macros", From 173386eeac282275c226b3a292e268140caaeed0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:59:05 -0700 Subject: [PATCH 12/14] chore(deps): bump tree-sitter from 0.25.6 to 0.25.8 in /codex-rs (#1561) Bumps [tree-sitter](https://github.com/tree-sitter/tree-sitter) from 0.25.6 to 0.25.8.
Commits
  • f2f197b 0.25.8
  • 8bb33f7 perf: reorder conditional operands
  • 6f944de fix(generate): propagate node types error
  • c159385 0.25.7
  • 94b55bf perf: reorder expensive conditional operand
  • bcb30f7 fix(generate): use topological sort for subtype map
  • 3bd8f7d perf: More efficient computation of used symbols
  • d7529c3 perf: reserve Vec capacities where appropriate
  • bf4217f fix(web): wasm export paths
  • bb7b339 Fix 'extra' field generation for node-types.json
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tree-sitter&package-manager=cargo&previous-version=0.25.6&new-version=0.25.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 4 ++-- codex-rs/apply-patch/Cargo.toml | 2 +- codex-rs/core/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index e1c17e1850..20b73ade8e 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4855,9 +4855,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.25.6" +version = "0.25.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cf18d43cbf0bfca51f657132cc616a5097edc4424d538bae6fa60142eaf9f0" +checksum = "6d7b8994f367f16e6fa14b5aebbcb350de5d7cbea82dc5b00ae997dd71680dd2" dependencies = [ "cc", "regex", diff --git a/codex-rs/apply-patch/Cargo.toml b/codex-rs/apply-patch/Cargo.toml index 7848e6e47f..5b95d4fa15 100644 --- a/codex-rs/apply-patch/Cargo.toml +++ b/codex-rs/apply-patch/Cargo.toml @@ -14,7 +14,7 @@ workspace = true anyhow = "1" similar = "2.7.0" thiserror = "2.0.12" -tree-sitter = "0.25.3" +tree-sitter = "0.25.8" tree-sitter-bash = "0.25.0" [dev-dependencies] diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 2d0877633a..d3933f842c 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -43,7 +43,7 @@ tokio = { version = "1", features = [ tokio-util = "0.7.14" toml = "0.9.1" tracing = { version = "0.1.41", features = ["log"] } -tree-sitter = "0.25.3" +tree-sitter = "0.25.8" tree-sitter-bash = "0.25.0" uuid = { version = "1", features = ["serde", "v4"] } wildmatch = "2.4.0" From d2be0720b5fdd2d1fad3e62ffeadbab89a8e65bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 17:22:05 -0700 Subject: [PATCH 13/14] chore(deps): bump toml from 0.9.1 to 0.9.2 in /codex-rs (#1562) Bumps [toml](https://github.com/toml-rs/toml) from 0.9.1 to 0.9.2.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toml&package-manager=cargo&previous-version=0.9.1&new-version=0.9.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- codex-rs/Cargo.lock | 14 +++++++------- codex-rs/core/Cargo.toml | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 20b73ade8e..d179a142f4 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -649,7 +649,7 @@ dependencies = [ "clap", "codex-core", "serde", - "toml 0.9.1", + "toml 0.9.2", ] [[package]] @@ -689,7 +689,7 @@ dependencies = [ "tokio", "tokio-test", "tokio-util", - "toml 0.9.1", + "toml 0.9.2", "tracing", "tree-sitter", "tree-sitter-bash", @@ -807,7 +807,7 @@ dependencies = [ "tempfile", "tokio", "tokio-test", - "toml 0.9.1", + "toml 0.9.2", "tracing", "tracing-subscriber", "uuid", @@ -4665,9 +4665,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0207d6ed1852c2a124c1fbec61621acb8330d2bf969a5d0643131e9affd985a5" +checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" dependencies = [ "indexmap 2.10.0", "serde", @@ -4711,9 +4711,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e" +checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" dependencies = [ "winnow", ] diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index d3933f842c..50049e9629 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -41,7 +41,7 @@ tokio = { version = "1", features = [ "signal", ] } tokio-util = "0.7.14" -toml = "0.9.1" +toml = "0.9.2" tracing = { version = "0.1.41", features = ["log"] } tree-sitter = "0.25.8" tree-sitter-bash = "0.25.0" From 3f00f1a74a5b1a2e7959c1956ea98e1904f9748b Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 23 Jul 2025 21:59:03 -0700 Subject: [PATCH 14/14] fix: create separate test_support crates to eliminate #[allow(dead_code)] --- codex-rs/Cargo.lock | 28 +++++++++++++++++++ codex-rs/core/Cargo.toml | 1 + codex-rs/core/tests/client.rs | 13 ++++----- codex-rs/core/tests/common/Cargo.toml | 13 +++++++++ .../tests/{test_support.rs => common/lib.rs} | 22 --------------- codex-rs/core/tests/live_agent.rs | 3 +- codex-rs/core/tests/stream_no_completed.rs | 7 ++--- codex-rs/mcp-server/Cargo.toml | 1 + codex-rs/mcp-server/tests/codex_tool.rs | 28 ++++++++----------- codex-rs/mcp-server/tests/common/Cargo.toml | 19 +++++++++++++ .../tests/common/{mod.rs => lib.rs} | 5 ++-- .../mcp-server/tests/common/mcp_process.rs | 16 ++--------- codex-rs/mcp-server/tests/common/responses.rs | 4 --- codex-rs/mcp-server/tests/interrupt.rs | 8 +++--- 14 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 codex-rs/core/tests/common/Cargo.toml rename codex-rs/core/tests/{test_support.rs => common/lib.rs} (67%) create mode 100644 codex-rs/mcp-server/tests/common/Cargo.toml rename codex-rs/mcp-server/tests/common/{mod.rs => lib.rs} (69%) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index d179a142f4..df1b0235a7 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -663,6 +663,7 @@ dependencies = [ "bytes", "codex-apply-patch", "codex-mcp-client", + "core_test_support", "dirs", "env-flags", "eventsource-stream", @@ -799,6 +800,7 @@ dependencies = [ "codex-core", "codex-linux-sandbox", "mcp-types", + "mcp_test_support", "pretty_assertions", "schemars 0.8.22", "serde", @@ -952,6 +954,16 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_test_support" +version = "0.0.0" +dependencies = [ + "codex-core", + "serde_json", + "tempfile", + "tokio", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -2604,6 +2616,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "mcp_test_support" +version = "0.0.0" +dependencies = [ + "anyhow", + "assert_cmd", + "codex-mcp-server", + "mcp-types", + "pretty_assertions", + "serde_json", + "shlex", + "tempfile", + "tokio", + "wiremock", +] + [[package]] name = "memchr" version = "2.7.5" diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 50049e9629..62e462bf97 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -62,6 +62,7 @@ openssl-sys = { version = "*", features = ["vendored"] } [dev-dependencies] assert_cmd = "2" +core_test_support = { path = "tests/common" } maplit = "1.0.2" predicates = "3" pretty_assertions = "1.4.1" diff --git a/codex-rs/core/tests/client.rs b/codex-rs/core/tests/client.rs index 5a6b6100eb..cb60fb216f 100644 --- a/codex-rs/core/tests/client.rs +++ b/codex-rs/core/tests/client.rs @@ -5,10 +5,10 @@ use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; use codex_core::protocol::SessionConfiguredEvent; -mod test_support; +use core_test_support::load_default_config_for_test; +use core_test_support::load_sse_fixture_with_id; +use core_test_support::wait_for_event; use tempfile::TempDir; -use test_support::load_default_config_for_test; -use test_support::load_sse_fixture_with_id; use wiremock::Mock; use wiremock::MockServer; use wiremock::ResponseTemplate; @@ -84,14 +84,13 @@ async fn includes_session_id_and_model_headers_in_request() { .unwrap(); let EventMsg::SessionConfigured(SessionConfiguredEvent { session_id, .. }) = - test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::SessionConfigured(_))) - .await + wait_for_event(&codex, |ev| matches!(ev, EventMsg::SessionConfigured(_))).await else { unreachable!() }; let current_session_id = Some(session_id.to_string()); - test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; // get request from the server let request = &server.received_requests().await.unwrap()[0]; @@ -160,7 +159,7 @@ async fn includes_base_instructions_override_in_request() { .await .unwrap(); - test_support::wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; + wait_for_event(&codex, |ev| matches!(ev, EventMsg::TaskComplete(_))).await; let request = &server.received_requests().await.unwrap()[0]; let request_body = request.body_json::().unwrap(); diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml new file mode 100644 index 0000000000..56da62994b --- /dev/null +++ b/codex-rs/core/tests/common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "core_test_support" +version = "0.0.0" +edition = "2024" + +[lib] +path = "lib.rs" + +[dependencies] +codex-core = { path = "../.." } +serde_json = "1" +tempfile = "3" +tokio = { version = "1", features = ["time"] } diff --git a/codex-rs/core/tests/test_support.rs b/codex-rs/core/tests/common/lib.rs similarity index 67% rename from codex-rs/core/tests/test_support.rs rename to codex-rs/core/tests/common/lib.rs index 83b8a14793..e3b5ca98c6 100644 --- a/codex-rs/core/tests/test_support.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -1,18 +1,11 @@ #![allow(clippy::expect_used)] -// Helpers shared by the integration tests. These are located inside the -// `tests/` tree on purpose so they never become part of the public API surface -// of the `codex-core` crate. - use tempfile::TempDir; use codex_core::config::Config; use codex_core::config::ConfigOverrides; use codex_core::config::ConfigToml; -/// Returns a default `Config` whose on-disk state is confined to the provided -/// temporary directory. Using a per-test directory keeps tests hermetic and -/// avoids clobbering a developer’s real `~/.codex`. pub fn load_default_config_for_test(codex_home: &TempDir) -> Config { Config::load_from_base_config_with_overrides( ConfigToml::default(), @@ -22,15 +15,6 @@ pub fn load_default_config_for_test(codex_home: &TempDir) -> Config { .expect("defaults for test should always succeed") } -/// Builds an SSE stream body from a JSON fixture. -/// -/// The fixture must contain an array of objects where each object represents a -/// single SSE event with at least a `type` field matching the `event:` value. -/// Additional fields become the JSON payload for the `data:` line. An object -/// with only a `type` field results in an event with no `data:` section. This -/// makes it trivial to extend the fixtures as OpenAI adds new event kinds or -/// fields. -#[allow(dead_code)] pub fn load_sse_fixture(path: impl AsRef) -> String { let events: Vec = serde_json::from_reader(std::fs::File::open(path).expect("read fixture")) @@ -51,11 +35,6 @@ pub fn load_sse_fixture(path: impl AsRef) -> String { .collect() } -/// Same as [`load_sse_fixture`], but replaces the placeholder `__ID__` in the -/// fixture template with the supplied identifier before parsing. This lets a -/// single JSON template be reused by multiple tests that each need a unique -/// `response_id`. -#[allow(dead_code)] pub fn load_sse_fixture_with_id(path: impl AsRef, id: &str) -> String { let raw = std::fs::read_to_string(path).expect("read fixture template"); let replaced = raw.replace("__ID__", id); @@ -77,7 +56,6 @@ pub fn load_sse_fixture_with_id(path: impl AsRef, id: &str) -> .collect() } -#[allow(dead_code)] pub async fn wait_for_event( codex: &codex_core::Codex, mut predicate: F, diff --git a/codex-rs/core/tests/live_agent.rs b/codex-rs/core/tests/live_agent.rs index 0be6110571..9d81225b9f 100644 --- a/codex-rs/core/tests/live_agent.rs +++ b/codex-rs/core/tests/live_agent.rs @@ -26,9 +26,8 @@ use codex_core::protocol::ErrorEvent; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; -mod test_support; +use core_test_support::load_default_config_for_test; use tempfile::TempDir; -use test_support::load_default_config_for_test; use tokio::sync::Notify; use tokio::time::timeout; diff --git a/codex-rs/core/tests/stream_no_completed.rs b/codex-rs/core/tests/stream_no_completed.rs index 1a0455be7c..153330bf10 100644 --- a/codex-rs/core/tests/stream_no_completed.rs +++ b/codex-rs/core/tests/stream_no_completed.rs @@ -9,11 +9,10 @@ use codex_core::exec::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR; use codex_core::protocol::EventMsg; use codex_core::protocol::InputItem; use codex_core::protocol::Op; -mod test_support; +use core_test_support::load_default_config_for_test; +use core_test_support::load_sse_fixture; +use core_test_support::load_sse_fixture_with_id; use tempfile::TempDir; -use test_support::load_default_config_for_test; -use test_support::load_sse_fixture; -use test_support::load_sse_fixture_with_id; use tokio::time::timeout; use wiremock::Mock; use wiremock::MockServer; diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index e524576a88..1088b92481 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -37,6 +37,7 @@ uuid = { version = "1", features = ["serde", "v4"] } [dev-dependencies] assert_cmd = "2" +mcp_test_support = { path = "tests/common" } pretty_assertions = "1.4.1" tempfile = "3" tokio-test = "0.4" diff --git a/codex-rs/mcp-server/tests/codex_tool.rs b/codex-rs/mcp-server/tests/codex_tool.rs index 0e31eea46e..828412f921 100644 --- a/codex-rs/mcp-server/tests/codex_tool.rs +++ b/codex-rs/mcp-server/tests/codex_tool.rs @@ -1,4 +1,4 @@ -mod common; +// Support code lives in the `mcp_test_support` crate under tests/common. use std::collections::HashMap; use std::env; @@ -26,11 +26,11 @@ use tempfile::TempDir; use tokio::time::timeout; use wiremock::MockServer; -use crate::common::McpProcess; -use crate::common::create_apply_patch_sse_response; -use crate::common::create_final_assistant_message_sse_response; -use crate::common::create_mock_chat_completions_server; -use crate::common::create_shell_sse_response; +use mcp_test_support::McpProcess; +use mcp_test_support::create_apply_patch_sse_response; +use mcp_test_support::create_final_assistant_message_sse_response; +use mcp_test_support::create_mock_chat_completions_server; +use mcp_test_support::create_shell_sse_response; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10); @@ -60,8 +60,7 @@ async fn shell_command_approval_triggers_elicitation() -> anyhow::Result<()> { let McpHandle { process: mut mcp_process, - server: _server, - dir: _dir, + .. } = create_mcp_process(vec![ create_shell_sse_response( shell_command.clone(), @@ -204,8 +203,7 @@ async fn patch_approval_triggers_elicitation() -> anyhow::Result<()> { let McpHandle { process: mut mcp_process, - server: _server, - dir: _dir, + .. } = create_mcp_process(vec![ create_apply_patch_sse_response(&patch_content, "call1234")?, create_final_assistant_message_sse_response("Patch has been applied successfully!")?, @@ -395,11 +393,9 @@ fn create_expected_patch_approval_elicitation_request( pub struct McpHandle { pub process: McpProcess, /// Retain the server for the lifetime of the McpProcess. - #[allow(dead_code)] - server: MockServer, + _server: MockServer, /// Retain the temporary directory for the lifetime of the McpProcess. - #[allow(dead_code)] - dir: TempDir, + _dir: TempDir, } async fn create_mcp_process(responses: Vec) -> anyhow::Result { @@ -410,8 +406,8 @@ async fn create_mcp_process(responses: Vec) -> anyhow::Result timeout(DEFAULT_READ_TIMEOUT, mcp_process.initialize()).await??; Ok(McpHandle { process: mcp_process, - server, - dir: codex_home, + _server: server, + _dir: codex_home, }) } diff --git a/codex-rs/mcp-server/tests/common/Cargo.toml b/codex-rs/mcp-server/tests/common/Cargo.toml new file mode 100644 index 0000000000..09eca9bc81 --- /dev/null +++ b/codex-rs/mcp-server/tests/common/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mcp_test_support" +version = "0.0.0" +edition = "2024" + +[lib] +path = "lib.rs" + +[dependencies] +anyhow = "1" +assert_cmd = "2" +codex-mcp-server = { path = "../.." } +mcp-types = { path = "../../../mcp-types" } +pretty_assertions = "1.4.1" +serde_json = "1" +shlex = "1.3.0" +tempfile = "3" +tokio = { version = "1", features = ["io-std", "macros", "process", "rt-multi-thread"] } +wiremock = "0.6" diff --git a/codex-rs/mcp-server/tests/common/mod.rs b/codex-rs/mcp-server/tests/common/lib.rs similarity index 69% rename from codex-rs/mcp-server/tests/common/mod.rs rename to codex-rs/mcp-server/tests/common/lib.rs index a9593e399a..1565559753 100644 --- a/codex-rs/mcp-server/tests/common/mod.rs +++ b/codex-rs/mcp-server/tests/common/lib.rs @@ -1,11 +1,12 @@ +// Re-export the items that tests use. This crate lives under +// `tests/common` so it is only built for tests via a path dev-dependency. + mod mcp_process; mod mock_model_server; mod responses; pub use mcp_process::McpProcess; pub use mock_model_server::create_mock_chat_completions_server; -#[allow(unused_imports)] pub use responses::create_apply_patch_sse_response; -#[allow(unused_imports)] pub use responses::create_final_assistant_message_sse_response; pub use responses::create_shell_sse_response; diff --git a/codex-rs/mcp-server/tests/common/mcp_process.rs b/codex-rs/mcp-server/tests/common/mcp_process.rs index 8f1f7a9e36..afdbadf9fd 100644 --- a/codex-rs/mcp-server/tests/common/mcp_process.rs +++ b/codex-rs/mcp-server/tests/common/mcp_process.rs @@ -33,10 +33,8 @@ use tokio::process::Command; pub struct McpProcess { next_request_id: AtomicI64, /// Retain this child process until the client is dropped. The Tokio runtime - /// will make a "best effort" to reap the process after it exits, but it is - /// not a guarantee. See the `kill_on_drop` documentation for details. - #[allow(dead_code)] - process: Child, + /// will make a best effort to reap the process after it exits. + _process: Child, stdin: ChildStdin, stdout: BufReader, } @@ -71,7 +69,7 @@ impl McpProcess { let stdout = BufReader::new(stdout); Ok(Self { next_request_id: AtomicI64::new(0), - process, + _process: process, stdin, stdout, }) @@ -191,8 +189,6 @@ impl McpProcess { Ok(request_id) } - // allow dead code - #[allow(dead_code)] pub async fn send_response( &mut self, id: RequestId, @@ -220,8 +216,6 @@ impl McpProcess { let message = serde_json::from_str::(&line)?; Ok(message) } - // allow dead code - #[allow(dead_code)] pub async fn read_stream_until_request_message(&mut self) -> anyhow::Result { loop { let message = self.read_jsonrpc_message().await?; @@ -244,8 +238,6 @@ impl McpProcess { } } - // allow dead code - #[allow(dead_code)] pub async fn read_stream_until_response_message( &mut self, request_id: RequestId, @@ -312,8 +304,6 @@ impl McpProcess { } } - // allow dead code - #[allow(dead_code)] pub async fn send_notification( &mut self, method: &str, diff --git a/codex-rs/mcp-server/tests/common/responses.rs b/codex-rs/mcp-server/tests/common/responses.rs index f47952a5ac..9a827fb986 100644 --- a/codex-rs/mcp-server/tests/common/responses.rs +++ b/codex-rs/mcp-server/tests/common/responses.rs @@ -39,8 +39,6 @@ pub fn create_shell_sse_response( Ok(sse) } -// allow dead code -#[allow(dead_code)] pub fn create_final_assistant_message_sse_response(message: &str) -> anyhow::Result { let assistant_message = json!({ "choices": [ @@ -60,8 +58,6 @@ pub fn create_final_assistant_message_sse_response(message: &str) -> anyhow::Res Ok(sse) } -// allow dead code -#[allow(dead_code)] pub fn create_apply_patch_sse_response( patch_content: &str, call_id: &str, diff --git a/codex-rs/mcp-server/tests/interrupt.rs b/codex-rs/mcp-server/tests/interrupt.rs index 64cf8b477e..cd163ea06b 100644 --- a/codex-rs/mcp-server/tests/interrupt.rs +++ b/codex-rs/mcp-server/tests/interrupt.rs @@ -1,5 +1,5 @@ #![cfg(unix)] -mod common; +// Support code lives in the `mcp_test_support` crate under tests/common. use std::path::Path; @@ -11,9 +11,9 @@ use serde_json::json; use tempfile::TempDir; use tokio::time::timeout; -use crate::common::McpProcess; -use crate::common::create_mock_chat_completions_server; -use crate::common::create_shell_sse_response; +use mcp_test_support::McpProcess; +use mcp_test_support::create_mock_chat_completions_server; +use mcp_test_support::create_shell_sse_response; const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);