diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 65a233a3..496bc810 100644 --- a/codex-rs/app-server-protocol/src/protocol.rs +++ b/codex-rs/app-server-protocol/src/protocol.rs @@ -340,12 +340,24 @@ pub struct ResumeConversationResponse { pub model: String, #[serde(skip_serializing_if = "Option::is_none")] pub initial_messages: Option>, + pub rollout_path: PathBuf, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -pub struct GetConversationSummaryParams { - pub rollout_path: PathBuf, +#[serde(untagged)] +pub enum GetConversationSummaryParams { + /// Provide the absolute or CODEX_HOME‑relative rollout path directly. + RolloutPath { + #[serde(rename = "rolloutPath")] + rollout_path: PathBuf, + }, + /// Provide a conversation id; the server will locate the rollout using the + /// same logic as `resumeConversation`. There will be extra latency compared to using the rollout path, + /// as the server needs to locate the rollout path first. + ConversationId { + #[serde(rename = "conversationId")] + conversation_id: ConversationId, + }, } #[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)] @@ -487,8 +499,12 @@ pub struct LogoutAccountResponse {} #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] pub struct ResumeConversationParams { - /// Absolute path to the rollout JSONL file. - pub path: PathBuf, + /// Absolute path to the rollout JSONL file. If omitted, `conversationId` must be provided. + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, + /// If the rollout path is not known, it can be discovered via the conversation id at at the cost of extra latency. + #[serde(skip_serializing_if = "Option::is_none")] + pub conversation_id: Option, /// Optional overrides to apply when spawning the resumed session. #[serde(skip_serializing_if = "Option::is_none")] pub overrides: Option, diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 4f8c066f..9edb73c5 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -834,12 +834,37 @@ impl CodexMessageProcessor { request_id: RequestId, params: GetConversationSummaryParams, ) { - let GetConversationSummaryParams { rollout_path } = params; - let path = if rollout_path.is_relative() { - self.config.codex_home.join(&rollout_path) - } else { - rollout_path.clone() + let path = match params { + GetConversationSummaryParams::RolloutPath { rollout_path } => { + if rollout_path.is_relative() { + self.config.codex_home.join(&rollout_path) + } else { + rollout_path + } + } + GetConversationSummaryParams::ConversationId { conversation_id } => { + match codex_core::find_conversation_path_by_id_str( + &self.config.codex_home, + &conversation_id.to_string(), + ) + .await + { + Ok(Some(p)) => p, + _ => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: format!( + "no rollout found for conversation id {conversation_id}" + ), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + } + } }; + let fallback_provider = self.config.model_provider_id.as_str(); match read_summary_from_rollout(&path, fallback_provider).await { @@ -990,6 +1015,43 @@ impl CodexMessageProcessor { request_id: RequestId, params: ResumeConversationParams, ) { + let path = match params { + ResumeConversationParams { + path: Some(path), .. + } => path, + ResumeConversationParams { + conversation_id: Some(conversation_id), + .. + } => { + match codex_core::find_conversation_path_by_id_str( + &self.config.codex_home, + &conversation_id.to_string(), + ) + .await + { + Ok(Some(p)) => p, + _ => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "unable to locate rollout path".to_string(), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + } + } + _ => { + let error = JSONRPCErrorError { + code: INVALID_REQUEST_ERROR_CODE, + message: "either path or conversation id must be provided".to_string(), + data: None, + }; + self.outgoing.send_error(request_id, error).await; + return; + } + }; + // Derive a Config using the same logic as new conversation, honoring overrides if provided. let config = match params.overrides { Some(overrides) => { @@ -1012,11 +1074,7 @@ impl CodexMessageProcessor { match self .conversation_manager - .resume_conversation_from_rollout( - config, - params.path.clone(), - self.auth_manager.clone(), - ) + .resume_conversation_from_rollout(config, path.clone(), self.auth_manager.clone()) .await { Ok(NewConversation { @@ -1046,6 +1104,7 @@ impl CodexMessageProcessor { conversation_id, model: session_configured.model.clone(), initial_messages, + rollout_path: session_configured.rollout_path.clone(), }; self.outgoing.send_response(request_id, response).await; } diff --git a/codex-rs/app-server/tests/suite/list_resume.rs b/codex-rs/app-server/tests/suite/list_resume.rs index 44578e04..e0d17ec2 100644 --- a/codex-rs/app-server/tests/suite/list_resume.rs +++ b/codex-rs/app-server/tests/suite/list_resume.rs @@ -171,7 +171,8 @@ async fn test_list_and_resume_conversations() -> Result<()> { // Now resume one of the sessions and expect a SessionConfigured notification and response. let resume_req_id = mcp .send_resume_conversation_request(ResumeConversationParams { - path: items[0].path.clone(), + path: Some(items[0].path.clone()), + conversation_id: None, overrides: Some(NewConversationParams { model: Some("o3".to_string()), ..Default::default() diff --git a/codex-rs/file-search/src/lib.rs b/codex-rs/file-search/src/lib.rs index 17265a85..0ae51210 100644 --- a/codex-rs/file-search/src/lib.rs +++ b/codex-rs/file-search/src/lib.rs @@ -40,6 +40,7 @@ pub struct FileMatch { pub indices: Option>, // Sorted & deduplicated when present } +#[derive(Debug)] pub struct FileSearchResults { pub matches: Vec, pub total_match_count: usize,