diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index b4ed4a93..9973a004 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -4,9 +4,9 @@ name = "codex-core" version = { workspace = true } [lib] +doctest = false name = "codex_core" path = "src/lib.rs" -doctest = false [lints] workspace = true @@ -41,7 +41,12 @@ similar = "2.7.0" strum_macros = "0.27.2" tempfile = "3" thiserror = "2.0.16" -time = { version = "0.3", features = ["formatting", "parsing", "local-offset", "macros"] } +time = { version = "0.3", features = [ + "formatting", + "parsing", + "local-offset", + "macros", +] } tokio = { version = "1", features = [ "io-std", "macros", diff --git a/codex-rs/mcp-server/Cargo.toml b/codex-rs/mcp-server/Cargo.toml index e068ba64..9eb5f6eb 100644 --- a/codex-rs/mcp-server/Cargo.toml +++ b/codex-rs/mcp-server/Cargo.toml @@ -36,7 +36,7 @@ tokio = { version = "1", features = [ toml = "0.9" tracing = { version = "0.1.41", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] } -uuid = { version = "1", features = ["serde", "v4"] } +uuid = { version = "1", features = ["serde", "v7"] } [dev-dependencies] assert_cmd = "2" diff --git a/codex-rs/mcp-server/src/codex_message_processor.rs b/codex-rs/mcp-server/src/codex_message_processor.rs index ceb47889..af0ffefe 100644 --- a/codex-rs/mcp-server/src/codex_message_processor.rs +++ b/codex-rs/mcp-server/src/codex_message_processor.rs @@ -814,7 +814,7 @@ impl CodexMessageProcessor { return; }; - let required_suffix = format!("{}.jsonl", conversation_id.0); + let required_suffix = format!("{conversation_id}.jsonl"); let Some(file_name) = canonical_rollout_path.file_name().map(OsStr::to_owned) else { let error = JSONRPCErrorError { code: INVALID_REQUEST_ERROR_CODE, @@ -1414,13 +1414,13 @@ mod tests { #[test] fn extract_conversation_summary_prefers_plain_user_messages() { let conversation_id = - ConversationId(Uuid::parse_str("3f941c35-29b3-493b-b0a4-e25800d9aeb0").unwrap()); + ConversationId::from_string("3f941c35-29b3-493b-b0a4-e25800d9aeb0").unwrap(); let timestamp = Some("2025-09-05T16:53:11.850Z".to_string()); let path = PathBuf::from("rollout.jsonl"); let head = vec![ json!({ - "id": conversation_id.0, + "id": conversation_id.to_string(), "timestamp": timestamp, "cwd": "/", "originator": "codex", diff --git a/codex-rs/mcp-server/src/message_processor.rs b/codex-rs/mcp-server/src/message_processor.rs index c696d453..5868d60f 100644 --- a/codex-rs/mcp-server/src/message_processor.rs +++ b/codex-rs/mcp-server/src/message_processor.rs @@ -36,7 +36,6 @@ use serde_json::json; use std::sync::Arc; use tokio::sync::Mutex; use tokio::task; -use uuid::Uuid; pub(crate) struct MessageProcessor { codex_message_processor: CodexMessageProcessor, @@ -484,8 +483,8 @@ impl MessageProcessor { return; } }; - let conversation_id = match Uuid::parse_str(&conversation_id) { - Ok(id) => ConversationId::from(id), + let conversation_id = match ConversationId::from_string(&conversation_id) { + Ok(id) => id, Err(e) => { tracing::error!("Failed to parse conversation_id: {e}"); let result = CallToolResult { diff --git a/codex-rs/mcp-server/tests/suite/list_resume.rs b/codex-rs/mcp-server/tests/suite/list_resume.rs index 9b5748ca..9302b429 100644 --- a/codex-rs/mcp-server/tests/suite/list_resume.rs +++ b/codex-rs/mcp-server/tests/suite/list_resume.rs @@ -142,7 +142,7 @@ async fn test_list_and_resume_conversations() { } = to_response::(resume_resp) .expect("deserialize resumeConversation response"); // conversation id should be a valid UUID - let _: uuid::Uuid = conversation_id.into(); + assert!(!conversation_id.to_string().is_empty()); } fn create_fake_rollout(codex_home: &Path, filename_ts: &str, meta_rfc3339: &str, preview: &str) { diff --git a/codex-rs/mcp-server/tests/suite/send_message.rs b/codex-rs/mcp-server/tests/suite/send_message.rs index 8f607776..158cb12d 100644 --- a/codex-rs/mcp-server/tests/suite/send_message.rs +++ b/codex-rs/mcp-server/tests/suite/send_message.rs @@ -136,7 +136,7 @@ async fn test_send_message_session_not_found() { .expect("timeout") .expect("init"); - let unknown = ConversationId(uuid::Uuid::new_v4()); + let unknown = ConversationId::new(); let req_id = mcp .send_send_user_message_request(SendUserMessageParams { conversation_id: unknown, diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index f88297a0..bbe2ed3f 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -23,8 +23,12 @@ strum = "0.27.2" strum_macros = "0.27.2" sys-locale = "0.3.2" tracing = "0.1.41" -ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl", "no-serde-warnings"] } -uuid = { version = "1", features = ["serde", "v4"] } +ts-rs = { version = "11", features = [ + "uuid-impl", + "serde-json-impl", + "no-serde-warnings", +] } +uuid = { version = "1", features = ["serde", "v7"] } [dev-dependencies] pretty_assertions = "1.4.1" diff --git a/codex-rs/protocol/src/mcp_protocol.rs b/codex-rs/protocol/src/mcp_protocol.rs index fe77d20b..c9137d0f 100644 --- a/codex-rs/protocol/src/mcp_protocol.rs +++ b/codex-rs/protocol/src/mcp_protocol.rs @@ -19,13 +19,23 @@ use strum_macros::Display; use ts_rs::TS; use uuid::Uuid; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, TS, Hash)] #[ts(type = "string")] -pub struct ConversationId(pub Uuid); +pub struct ConversationId { + uuid: Uuid, +} impl ConversationId { pub fn new() -> Self { - Self(Uuid::new_v4()) + Self { + uuid: Uuid::now_v7(), + } + } + + pub fn from_string(s: &str) -> Result { + Ok(Self { + uuid: Uuid::parse_str(s)?, + }) } } @@ -37,19 +47,27 @@ impl Default for ConversationId { impl Display for ConversationId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) + write!(f, "{}", self.uuid) } } -impl From for ConversationId { - fn from(value: Uuid) -> Self { - Self(value) +impl Serialize for ConversationId { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(&self.uuid) } } -impl From for Uuid { - fn from(value: ConversationId) -> Self { - value.0 +impl<'de> Deserialize<'de> for ConversationId { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = String::deserialize(deserializer)?; + let uuid = Uuid::parse_str(&value).map_err(serde::de::Error::custom)?; + Ok(Self { uuid }) } } @@ -719,6 +737,27 @@ mod tests { #[test] fn test_conversation_id_default_is_not_zeroes() { let id = ConversationId::default(); - assert_ne!(id.0, Uuid::nil()); + assert_ne!(id.uuid, Uuid::nil()); + } + + #[test] + fn conversation_id_serializes_as_plain_string() { + let id = ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8").unwrap(); + + assert_eq!( + json!("67e55044-10b1-426f-9247-bb680e5fe0c8"), + serde_json::to_value(id).unwrap() + ); + } + + #[test] + fn conversation_id_deserializes_from_plain_string() { + let id: ConversationId = + serde_json::from_value(json!("67e55044-10b1-426f-9247-bb680e5fe0c8")).unwrap(); + + assert_eq!( + ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8").unwrap(), + id, + ); } } diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index c3aebcdd..dddaeee2 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1252,7 +1252,8 @@ mod tests { /// amount of nesting. #[test] fn serialize_event() { - let conversation_id = ConversationId(uuid::uuid!("67e55044-10b1-426f-9247-bb680e5fe0c8")); + let conversation_id = + ConversationId::from_string("67e55044-10b1-426f-9247-bb680e5fe0c8").unwrap(); let rollout_file = NamedTempFile::new().unwrap(); let event = Event { id: "1234".to_string(),