From 1d76ba5ebe4ee345487d3ef68c6bc15e4b528db5 Mon Sep 17 00:00:00 2001 From: Gabriel Peal Date: Tue, 28 Oct 2025 17:17:22 -0700 Subject: [PATCH] [App Server] Allow fetching or resuming a conversation summary from the conversation id (#5890) This PR adds an option to app server to allow conversation summaries to be fetched from just the conversation id rather than rollout path for convenience at the cost of some latency to discover the rollout path. This convenience is non-trivial as it allows app servers to simply maintain conversation ids rather than rollout paths and the associated platform (Windows) handling associated with storing and encoding them correctly. --- codex-rs/app-server-protocol/src/protocol.rs | 26 ++++-- .../app-server/src/codex_message_processor.rs | 79 ++++++++++++++++--- .../app-server/tests/suite/list_resume.rs | 3 +- codex-rs/file-search/src/lib.rs | 1 + 4 files changed, 93 insertions(+), 16 deletions(-) diff --git a/codex-rs/app-server-protocol/src/protocol.rs b/codex-rs/app-server-protocol/src/protocol.rs index 65a233a37..496bc8100 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 4f8c066f7..9edb73c5d 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 44578e04f..e0d17ec22 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 17265a85f..0ae512102 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,