From e7d1b23e60243f34476bd0241714396fee195022 Mon Sep 17 00:00:00 2001 From: rabsef-bicyrm Date: Wed, 14 Jan 2026 09:11:31 -0800 Subject: [PATCH 1/3] did: Add MCP resume-path support - Add resume-path to codex tool config and message processor - Resume threads from rollout in MCP runner - Expose ThreadManager helper for resume --- codex-rs/core/src/thread_manager.rs | 13 +++++ codex-rs/mcp-server/src/codex_tool_config.rs | 9 +++- codex-rs/mcp-server/src/codex_tool_runner.rs | 53 ++++++++++++++------ codex-rs/mcp-server/src/message_processor.rs | 4 +- 4 files changed, 61 insertions(+), 18 deletions(-) diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 9b633ded378..3eb4d29669a 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -226,6 +226,19 @@ impl ThreadManager { .await } + pub async fn resume_thread_from_rollout_with_auth( + &self, + config: Config, + rollout_path: PathBuf, + ) -> CodexResult { + self.resume_thread_from_rollout( + config, + rollout_path, + Arc::clone(&self.state.auth_manager), + ) + .await + } + pub async fn resume_thread_with_history( &self, config: Config, diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index 8131d7da52f..f3d755e72a4 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -61,6 +61,10 @@ pub struct CodexToolCallParam { /// Prompt used when compacting the conversation. #[serde(default, skip_serializing_if = "Option::is_none")] pub compact_prompt: Option, + + /// Optional path to a rollout file to resume the session from. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub resume_path: Option, } /// Custom enum mirroring [`AskForApproval`], but has an extra dependency on @@ -153,7 +157,7 @@ impl CodexToolCallParam { pub async fn into_config( self, codex_linux_sandbox_exe: Option, - ) -> std::io::Result<(String, Config)> { + ) -> std::io::Result<(String, Config, Option)> { let Self { prompt, model, @@ -165,6 +169,7 @@ impl CodexToolCallParam { base_instructions, developer_instructions, compact_prompt, + resume_path, } = self; // Build the `ConfigOverrides` recognized by codex-core. @@ -190,7 +195,7 @@ impl CodexToolCallParam { let cfg = Config::load_with_cli_overrides_and_harness_overrides(cli_overrides, overrides).await?; - Ok((prompt, cfg)) + Ok((prompt, cfg, resume_path)) } } diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index dcaf8a89e6c..b409e0136b7 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -3,6 +3,7 @@ //! and to make future feature-growth easier to manage. use std::collections::HashMap; +use std::path::PathBuf; use std::sync::Arc; use crate::exec_approval::handle_exec_approval_request; @@ -66,6 +67,7 @@ pub async fn run_codex_tool_session( id: RequestId, initial_prompt: String, config: CodexConfig, + resume_path: Option, outgoing: Arc, thread_manager: Arc, running_requests_id_to_codex_uuid: Arc>>, @@ -74,21 +76,42 @@ pub async fn run_codex_tool_session( thread_id, thread, session_configured, - } = match thread_manager.start_thread(config).await { - Ok(res) => res, - Err(e) => { - let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: format!("Failed to start Codex session: {e}"), - annotations: None, - })], - is_error: Some(true), - structured_content: None, - }; - outgoing.send_response(id.clone(), result).await; - return; - } + } = match resume_path { + Some(path) => match thread_manager + .resume_thread_from_rollout_with_auth(config, path) + .await + { + Ok(res) => res, + Err(e) => { + let result = CallToolResult { + content: vec![ContentBlock::TextContent(TextContent { + r#type: "text".to_string(), + text: format!("Failed to resume Codex session: {e}"), + annotations: None, + })], + is_error: Some(true), + structured_content: None, + }; + outgoing.send_response(id.clone(), result).await; + return; + } + }, + None => match thread_manager.start_thread(config).await { + Ok(res) => res, + Err(e) => { + let result = CallToolResult { + content: vec![ContentBlock::TextContent(TextContent { + r#type: "text".to_string(), + text: format!("Failed to start Codex session: {e}"), + annotations: None, + })], + is_error: Some(true), + structured_content: None, + }; + outgoing.send_response(id.clone(), result).await; + return; + } + }, }; let session_configured_event = Event { diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 9d947cda3f4..6670b8c4a50 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -347,7 +347,8 @@ impl MessageProcessor { } } async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option) { - let (initial_prompt, config): (String, Config) = match arguments { + let (initial_prompt, config, resume_path): (String, Config, Option) = + match arguments { Some(json_val) => match serde_json::from_value::(json_val) { Ok(tool_cfg) => match tool_cfg .into_config(self.codex_linux_sandbox_exe.clone()) @@ -417,6 +418,7 @@ impl MessageProcessor { id, initial_prompt, config, + resume_path, outgoing, thread_manager, running_requests_id_to_codex_uuid, From 21fc6e41c80eb57cc55112d9f5c26264ecf2ec3b Mon Sep 17 00:00:00 2001 From: rabsef-bicyrm Date: Wed, 14 Jan 2026 12:04:57 -0800 Subject: [PATCH 2/3] fix: Update MCP codex tool schema tests - Add resume-path to expected schema - Add payload-compat test for legacy callers --- codex-rs/mcp-server/src/codex_tool_config.rs | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index f3d755e72a4..c86fa72c0de 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -330,6 +330,10 @@ mod tests { "description": "The *initial user prompt* to start the Codex conversation.", "type": "string" }, + "resume-path": { + "description": "Optional path to a rollout file to resume the session from.", + "type": "string" + }, "sandbox": { "description": "Sandbox mode: `read-only`, `workspace-write`, or `danger-full-access`.", "enum": [ @@ -412,4 +416,21 @@ mod tests { }); assert_eq!(expected_tool_json, tool_json); } + + #[test] + fn codex_tool_accepts_pre_change_payload() { + let payload = serde_json::json!({ + "prompt": "hello", + "model": "gpt-5.2", + "sandbox": "workspace-write", + "approval-policy": "untrusted", + }); + + let parsed: CodexToolCallParam = serde_json::from_value(payload).expect("payload parses"); + assert_eq!(parsed.prompt, "hello"); + assert_eq!(parsed.model.as_deref(), Some("gpt-5.2")); + assert_eq!(parsed.sandbox, Some(CodexToolCallSandboxMode::WorkspaceWrite)); + assert_eq!(parsed.approval_policy, Some(CodexToolCallApprovalPolicy::Untrusted)); + assert!(parsed.resume_path.is_none()); + } } From 1935752f102dace1dff880a655deb83b4a50a23d Mon Sep 17 00:00:00 2001 From: rabsef-bicyrm Date: Wed, 28 Jan 2026 10:16:35 -0800 Subject: [PATCH 3/3] chore: Apply rustfmt - Reformat MCP resume-path changes after rebase --- codex-rs/core/src/thread_manager.rs | 8 +-- codex-rs/mcp-server/src/codex_tool_config.rs | 10 ++- codex-rs/mcp-server/src/message_processor.rs | 64 ++++++++++---------- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/codex-rs/core/src/thread_manager.rs b/codex-rs/core/src/thread_manager.rs index 3eb4d29669a..09c464cd10c 100644 --- a/codex-rs/core/src/thread_manager.rs +++ b/codex-rs/core/src/thread_manager.rs @@ -231,12 +231,8 @@ impl ThreadManager { config: Config, rollout_path: PathBuf, ) -> CodexResult { - self.resume_thread_from_rollout( - config, - rollout_path, - Arc::clone(&self.state.auth_manager), - ) - .await + self.resume_thread_from_rollout(config, rollout_path, Arc::clone(&self.state.auth_manager)) + .await } pub async fn resume_thread_with_history( diff --git a/codex-rs/mcp-server/src/codex_tool_config.rs b/codex-rs/mcp-server/src/codex_tool_config.rs index c86fa72c0de..e9ab0966081 100644 --- a/codex-rs/mcp-server/src/codex_tool_config.rs +++ b/codex-rs/mcp-server/src/codex_tool_config.rs @@ -429,8 +429,14 @@ mod tests { let parsed: CodexToolCallParam = serde_json::from_value(payload).expect("payload parses"); assert_eq!(parsed.prompt, "hello"); assert_eq!(parsed.model.as_deref(), Some("gpt-5.2")); - assert_eq!(parsed.sandbox, Some(CodexToolCallSandboxMode::WorkspaceWrite)); - assert_eq!(parsed.approval_policy, Some(CodexToolCallApprovalPolicy::Untrusted)); + assert_eq!( + parsed.sandbox, + Some(CodexToolCallSandboxMode::WorkspaceWrite) + ); + assert_eq!( + parsed.approval_policy, + Some(CodexToolCallApprovalPolicy::Untrusted) + ); assert!(parsed.resume_path.is_none()); } } diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index 6670b8c4a50..15e08fa1235 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -349,19 +349,34 @@ impl MessageProcessor { async fn handle_tool_call_codex(&self, id: RequestId, arguments: Option) { let (initial_prompt, config, resume_path): (String, Config, Option) = match arguments { - Some(json_val) => match serde_json::from_value::(json_val) { - Ok(tool_cfg) => match tool_cfg - .into_config(self.codex_linux_sandbox_exe.clone()) - .await - { - Ok(cfg) => cfg, + Some(json_val) => match serde_json::from_value::(json_val) { + Ok(tool_cfg) => match tool_cfg + .into_config(self.codex_linux_sandbox_exe.clone()) + .await + { + Ok(cfg) => cfg, + Err(e) => { + let result = CallToolResult { + content: vec![ContentBlock::TextContent(TextContent { + r#type: "text".to_owned(), + text: format!( + "Failed to load Codex configuration from overrides: {e}" + ), + annotations: None, + })], + is_error: Some(true), + structured_content: None, + }; + self.send_response::(id, result) + .await; + return; + } + }, Err(e) => { let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { r#type: "text".to_owned(), - text: format!( - "Failed to load Codex configuration from overrides: {e}" - ), + text: format!("Failed to parse configuration for Codex tool: {e}"), annotations: None, })], is_error: Some(true), @@ -372,13 +387,15 @@ impl MessageProcessor { return; } }, - Err(e) => { + None => { let result = CallToolResult { content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_owned(), - text: format!("Failed to parse configuration for Codex tool: {e}"), - annotations: None, - })], + r#type: "text".to_string(), + text: + "Missing arguments for codex tool-call; the `prompt` field is required." + .to_string(), + annotations: None, + })], is_error: Some(true), structured_content: None, }; @@ -386,24 +403,7 @@ impl MessageProcessor { .await; return; } - }, - None => { - let result = CallToolResult { - content: vec![ContentBlock::TextContent(TextContent { - r#type: "text".to_string(), - text: - "Missing arguments for codex tool-call; the `prompt` field is required." - .to_string(), - annotations: None, - })], - is_error: Some(true), - structured_content: None, - }; - self.send_response::(id, result) - .await; - return; - } - }; + }; // Clone outgoing and server to move into async task. let outgoing = self.outgoing.clone();