Skip to content

Commit 1d76ba5

Browse files
authored
[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.
1 parent a1635ee commit 1d76ba5

File tree

4 files changed

+93
-16
lines changed

4 files changed

+93
-16
lines changed

codex-rs/app-server-protocol/src/protocol.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,24 @@ pub struct ResumeConversationResponse {
340340
pub model: String,
341341
#[serde(skip_serializing_if = "Option::is_none")]
342342
pub initial_messages: Option<Vec<EventMsg>>,
343+
pub rollout_path: PathBuf,
343344
}
344345

345346
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
346-
#[serde(rename_all = "camelCase")]
347-
pub struct GetConversationSummaryParams {
348-
pub rollout_path: PathBuf,
347+
#[serde(untagged)]
348+
pub enum GetConversationSummaryParams {
349+
/// Provide the absolute or CODEX_HOME‑relative rollout path directly.
350+
RolloutPath {
351+
#[serde(rename = "rolloutPath")]
352+
rollout_path: PathBuf,
353+
},
354+
/// Provide a conversation id; the server will locate the rollout using the
355+
/// same logic as `resumeConversation`. There will be extra latency compared to using the rollout path,
356+
/// as the server needs to locate the rollout path first.
357+
ConversationId {
358+
#[serde(rename = "conversationId")]
359+
conversation_id: ConversationId,
360+
},
349361
}
350362

351363
#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema, TS)]
@@ -487,8 +499,12 @@ pub struct LogoutAccountResponse {}
487499
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
488500
#[serde(rename_all = "camelCase")]
489501
pub struct ResumeConversationParams {
490-
/// Absolute path to the rollout JSONL file.
491-
pub path: PathBuf,
502+
/// Absolute path to the rollout JSONL file. If omitted, `conversationId` must be provided.
503+
#[serde(skip_serializing_if = "Option::is_none")]
504+
pub path: Option<PathBuf>,
505+
/// If the rollout path is not known, it can be discovered via the conversation id at at the cost of extra latency.
506+
#[serde(skip_serializing_if = "Option::is_none")]
507+
pub conversation_id: Option<ConversationId>,
492508
/// Optional overrides to apply when spawning the resumed session.
493509
#[serde(skip_serializing_if = "Option::is_none")]
494510
pub overrides: Option<NewConversationParams>,

codex-rs/app-server/src/codex_message_processor.rs

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -834,12 +834,37 @@ impl CodexMessageProcessor {
834834
request_id: RequestId,
835835
params: GetConversationSummaryParams,
836836
) {
837-
let GetConversationSummaryParams { rollout_path } = params;
838-
let path = if rollout_path.is_relative() {
839-
self.config.codex_home.join(&rollout_path)
840-
} else {
841-
rollout_path.clone()
837+
let path = match params {
838+
GetConversationSummaryParams::RolloutPath { rollout_path } => {
839+
if rollout_path.is_relative() {
840+
self.config.codex_home.join(&rollout_path)
841+
} else {
842+
rollout_path
843+
}
844+
}
845+
GetConversationSummaryParams::ConversationId { conversation_id } => {
846+
match codex_core::find_conversation_path_by_id_str(
847+
&self.config.codex_home,
848+
&conversation_id.to_string(),
849+
)
850+
.await
851+
{
852+
Ok(Some(p)) => p,
853+
_ => {
854+
let error = JSONRPCErrorError {
855+
code: INVALID_REQUEST_ERROR_CODE,
856+
message: format!(
857+
"no rollout found for conversation id {conversation_id}"
858+
),
859+
data: None,
860+
};
861+
self.outgoing.send_error(request_id, error).await;
862+
return;
863+
}
864+
}
865+
}
842866
};
867+
843868
let fallback_provider = self.config.model_provider_id.as_str();
844869

845870
match read_summary_from_rollout(&path, fallback_provider).await {
@@ -990,6 +1015,43 @@ impl CodexMessageProcessor {
9901015
request_id: RequestId,
9911016
params: ResumeConversationParams,
9921017
) {
1018+
let path = match params {
1019+
ResumeConversationParams {
1020+
path: Some(path), ..
1021+
} => path,
1022+
ResumeConversationParams {
1023+
conversation_id: Some(conversation_id),
1024+
..
1025+
} => {
1026+
match codex_core::find_conversation_path_by_id_str(
1027+
&self.config.codex_home,
1028+
&conversation_id.to_string(),
1029+
)
1030+
.await
1031+
{
1032+
Ok(Some(p)) => p,
1033+
_ => {
1034+
let error = JSONRPCErrorError {
1035+
code: INVALID_REQUEST_ERROR_CODE,
1036+
message: "unable to locate rollout path".to_string(),
1037+
data: None,
1038+
};
1039+
self.outgoing.send_error(request_id, error).await;
1040+
return;
1041+
}
1042+
}
1043+
}
1044+
_ => {
1045+
let error = JSONRPCErrorError {
1046+
code: INVALID_REQUEST_ERROR_CODE,
1047+
message: "either path or conversation id must be provided".to_string(),
1048+
data: None,
1049+
};
1050+
self.outgoing.send_error(request_id, error).await;
1051+
return;
1052+
}
1053+
};
1054+
9931055
// Derive a Config using the same logic as new conversation, honoring overrides if provided.
9941056
let config = match params.overrides {
9951057
Some(overrides) => {
@@ -1012,11 +1074,7 @@ impl CodexMessageProcessor {
10121074

10131075
match self
10141076
.conversation_manager
1015-
.resume_conversation_from_rollout(
1016-
config,
1017-
params.path.clone(),
1018-
self.auth_manager.clone(),
1019-
)
1077+
.resume_conversation_from_rollout(config, path.clone(), self.auth_manager.clone())
10201078
.await
10211079
{
10221080
Ok(NewConversation {
@@ -1046,6 +1104,7 @@ impl CodexMessageProcessor {
10461104
conversation_id,
10471105
model: session_configured.model.clone(),
10481106
initial_messages,
1107+
rollout_path: session_configured.rollout_path.clone(),
10491108
};
10501109
self.outgoing.send_response(request_id, response).await;
10511110
}

codex-rs/app-server/tests/suite/list_resume.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ async fn test_list_and_resume_conversations() -> Result<()> {
171171
// Now resume one of the sessions and expect a SessionConfigured notification and response.
172172
let resume_req_id = mcp
173173
.send_resume_conversation_request(ResumeConversationParams {
174-
path: items[0].path.clone(),
174+
path: Some(items[0].path.clone()),
175+
conversation_id: None,
175176
overrides: Some(NewConversationParams {
176177
model: Some("o3".to_string()),
177178
..Default::default()

codex-rs/file-search/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub struct FileMatch {
4040
pub indices: Option<Vec<u32>>, // Sorted & deduplicated when present
4141
}
4242

43+
#[derive(Debug)]
4344
pub struct FileSearchResults {
4445
pub matches: Vec<FileMatch>,
4546
pub total_match_count: usize,

0 commit comments

Comments
 (0)