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)
[](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)
[](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
[](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
[](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
[](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
[](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
[](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);