From 340a18ed7cd0aea3bc218838d9ac1f0f30efb1ac Mon Sep 17 00:00:00 2001 From: Ruslan Nigmatullin Date: Wed, 11 Nov 2026 09:43:44 +0000 Subject: [PATCH] Preserve model-facing ID allocations in saved history (#60771) ## Why Restoring model-facing ID sequences requires allocation ordinals alongside the rendered IDs. Responses API payloads still require string IDs. ## What changed - Add allocation metadata to `ResponseItemId` while keeping equality, hashing, and ordering based on the rendered ID. - Preserve optional `id_allocation` metadata in rollout items, inter-agent messages, compaction replacement history, and Guardian checkpoints. Continue accepting legacy string IDs. - Support structured context-window allocations alongside UUIDv7 strings, validate the `window` salt on restoration, and keep request metadata IDs as strings. ## Testing Add tests for allocation round trips, legacy ID compatibility, string-only raw response item IDs, and context-window allocation validation. GitOrigin-RevId: ce1fd964bde9a4e7c527432be99aea87ada30169 --- codex-rs/Cargo.lock | 1 + codex-rs/Cargo.toml | 1 + .../app-server-exports-stable.json.zst | Bin 168323 -> 169045 bytes codex-rs/core/src/responses_metadata.rs | 10 +- codex-rs/history/src/guardian_history.rs | 11 +- codex-rs/history/src/rollout_payload.rs | 110 ++++++++++----- codex-rs/history/src/tests.rs | 133 +++++++++++++++++- codex-rs/protocol/Cargo.toml | 1 + codex-rs/protocol/src/context_window_id.rs | 42 ++++-- .../protocol/src/context_window_id_tests.rs | 16 +++ codex-rs/protocol/src/lib.rs | 1 + codex-rs/protocol/src/models.rs | 46 ++++-- codex-rs/protocol/src/protocol.rs | 6 +- codex-rs/protocol/src/response_item_id.rs | 130 +++++++++++++++-- .../protocol/src/response_item_id_tests.rs | 27 ++++ 15 files changed, 450 insertions(+), 85 deletions(-) create mode 100644 codex-rs/protocol/src/context_window_id_tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 19a1146dc5..80ab9016a6 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4663,6 +4663,7 @@ dependencies = [ "codex-network-proxy", "codex-utils-absolute-path", "codex-utils-image", + "codex-utils-model-facing-id", "codex-utils-path-uri", "codex-utils-redacted-string", "codex-utils-string", diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 272fa60a2a..baaefe8d3b 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -321,6 +321,7 @@ codex-utils-head-tail-buffer = { path = "utils/head-tail-buffer" } codex-utils-home-dir = { path = "utils/home-dir" } codex-utils-image = { path = "utils/image" } codex-utils-json-to-toml = { path = "utils/json-to-toml" } +codex-utils-model-facing-id = { path = "utils/model-facing-id" } codex-utils-oss = { path = "utils/oss" } codex-utils-output-truncation = { path = "utils/output-truncation" } codex-utils-path = { path = "utils/path-utils" } diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst index 8e995c180a61e68cfbddf84d1ab5bbcdce84b733..29094221fa7dc75ded9b1534188d335ae51b97cc 100644 GIT binary patch delta 23764 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 PHwLN1ZbmF!$MR@@Z7(^8 diff --git a/codex-rs/core/src/responses_metadata.rs b/codex-rs/core/src/responses_metadata.rs index 8f886779c0..795cbf1eca 100644 --- a/codex-rs/core/src/responses_metadata.rs +++ b/codex-rs/core/src/responses_metadata.rs @@ -438,9 +438,11 @@ impl CodexResponsesMetadata { turn_id: self.turn_id.as_deref(), window_id: has_request_identity.then_some(self.window_id.as_str()), window_number: has_request_identity.then_some(self.window_number).flatten(), - context_window_id: has_request_identity - .then_some(self.context_window_id.as_ref()) - .flatten(), + context_window_id: self + .context_window_id + .as_ref() + .filter(|_| has_request_identity) + .map(ToString::to_string), request_kind: request_kind_value, history_initialization: self.history_initialization, forked_from_thread_id: self.forked_from_thread_id, @@ -572,7 +574,7 @@ struct CodexTurnMetadataPayload<'a> { #[serde(default, skip_serializing_if = "Option::is_none")] window_number: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - context_window_id: Option<&'a ContextWindowId>, + context_window_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] request_kind: Option<&'static str>, #[serde(default, skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/history/src/guardian_history.rs b/codex-rs/history/src/guardian_history.rs index e0657a3061..e077afdf27 100644 --- a/codex-rs/history/src/guardian_history.rs +++ b/codex-rs/history/src/guardian_history.rs @@ -1,6 +1,6 @@ //! Model-invisible Guardian transcript checkpoints and retained-section delivery proof. -//! Entries preserve rollback provenance; their flattened wire shape still reads as -//! ResponseItem on older hosts, and old metadata-free checkpoints remain readable. +//! Entries preserve rollback provenance and allocation ordinals. Readers also accept +//! metadata-free checkpoints and legacy string IDs. use std::borrow::Cow; @@ -11,6 +11,7 @@ use serde::Serialize; use crate::CodexHarnessMetadata; use crate::ResponseItemEnvelope; +use crate::rollout_payload::ItemPayload; /// Host omission notices delivered by a complete retained-instructions section. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] @@ -27,7 +28,7 @@ pub struct GuardianHistoryCheckpoint(pub Vec); #[derive(Serialize, Deserialize, JsonSchema)] struct Entry<'a> { #[serde(flatten)] - item: Cow<'a, ResponseItem>, + item: ItemPayload<'a, ResponseItem>, #[serde(default, skip_serializing_if = "Option::is_none")] guardian_metadata: Option>, } @@ -35,7 +36,7 @@ struct Entry<'a> { impl Serialize for GuardianHistoryCheckpoint { fn serialize(&self, serializer: S) -> Result { serializer.collect_seq(self.0.iter().map(|entry| Entry { - item: Cow::Borrowed(&entry.item), + item: (&entry.item).into(), guardian_metadata: entry.metadata.as_ref().map(Cow::Borrowed), })) } @@ -47,7 +48,7 @@ impl<'de> Deserialize<'de> for GuardianHistoryCheckpoint { Vec::>::deserialize(deserializer)? .into_iter() .map(|entry| ResponseItemEnvelope { - item: entry.item.into_owned(), + item: entry.item.into(), metadata: entry.guardian_metadata.map(Cow::into_owned), }) .collect(), diff --git a/codex-rs/history/src/rollout_payload.rs b/codex-rs/history/src/rollout_payload.rs index e7bf15f0fd..869195968b 100644 --- a/codex-rs/history/src/rollout_payload.rs +++ b/codex-rs/history/src/rollout_payload.rs @@ -18,6 +18,7 @@ use crate::RetainedContextEvent; use crate::RetainedUserMessage; use codex_protocol::ContextWindowId; use codex_protocol::ResponseItemId; +use codex_protocol::ResponseItemIdAllocation; use codex_protocol::models::ContentItem; use codex_protocol::models::InternalChatMessageMetadataPassthrough; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -43,12 +44,12 @@ pub(super) enum RolloutItemWire<'a> { payload: DynamicToolsUpdatedPayload<'a>, }, ResponseItem { - payload: Cow<'a, ResponseItem>, + payload: ItemPayload<'a, ResponseItem>, #[serde(default, skip_serializing_if = "Option::is_none")] metadata: Option>, }, InterAgentCommunication { - payload: Cow<'a, InterAgentCommunication>, + payload: ItemPayload<'a, InterAgentCommunication>, }, InterAgentCommunicationMetadata { payload: InterAgentCommunicationMetadataPayload, @@ -140,6 +141,42 @@ pub(super) struct DynamicToolsUpdatedPayload<'a> { tools: Cow<'a, [codex_protocol::dynamic_tools::DynamicToolSpec]>, } +/// The protocol item keeps its wire ID. The optional allocation lets replay resume the sequence. +#[derive(Serialize, Deserialize, JsonSchema)] +pub(super) struct ItemPayload<'a, T: Clone> { + #[serde(flatten)] + item: Cow<'a, T>, + #[serde(default, skip_serializing_if = "Option::is_none")] + id_allocation: Option>, +} + +impl<'a, T: Clone> ItemPayload<'a, T> { + fn borrowed(item: &'a T, id: Option<&'a ResponseItemId>) -> Self { + Self { + item: Cow::Borrowed(item), + id_allocation: id.and_then(ResponseItemId::allocation).map(Cow::Borrowed), + } + } +} + +impl<'a> From<&'a ResponseItem> for ItemPayload<'a, ResponseItem> { + fn from(item: &'a ResponseItem) -> Self { + Self::borrowed(item, item.id()) + } +} + +impl From> for ResponseItem { + fn from(payload: ItemPayload<'_, ResponseItem>) -> Self { + let mut item = payload.item.into_owned(); + if let Some(allocation) = payload.id_allocation + && let Some(id) = item.id().cloned() + { + item.set_id(Some(id.with_allocation(allocation.into_owned()))); + } + item + } +} + impl<'a> From<&'a RolloutItem> for RolloutItemWire<'a> { fn from(item: &'a RolloutItem) -> Self { match item { @@ -153,11 +190,11 @@ impl<'a> From<&'a RolloutItem> for RolloutItemWire<'a> { }, }, RolloutItem::ResponseItem(envelope) => Self::ResponseItem { - payload: Cow::Borrowed(&envelope.item), + payload: (&envelope.item).into(), metadata: envelope.metadata.as_ref().map(Cow::Borrowed), }, RolloutItem::InterAgentCommunication(payload) => Self::InterAgentCommunication { - payload: Cow::Borrowed(payload), + payload: ItemPayload::borrowed(payload, payload.id.as_ref()), }, RolloutItem::InterAgentCommunicationMetadata { trigger_turn } => { Self::InterAgentCommunicationMetadata { @@ -194,27 +231,30 @@ impl<'a> From<&'a RolloutItem> for RolloutItemWire<'a> { format!("{CODE_MODE_DELIVERY_INCOMPLETE}{}", message.text) }; Self::ResponseItem { - payload: Cow::Owned(ResponseItem::Message { - status: None, - encrypted_content: None, - id: message.message_id.clone().map(ResponseItemId::from_server), - role: "assistant".to_owned(), - content: vec![ContentItem::OutputText { - annotations: None, - logprobs: None, - text, - }], - // Older Guardian clients exclude commentary from root-to-worker - // review. Keep the phase unspecified so they can review this - // confirmed delivery without claiming it ended the turn. - phase: None, - internal_chat_message_metadata_passthrough: Some( - InternalChatMessageMetadataPassthrough { - turn_id: Some(message.turn_id.clone()), - ..Default::default() - }, - ), - }), + payload: ItemPayload { + item: Cow::Owned(ResponseItem::Message { + status: None, + encrypted_content: None, + id: message.message_id.clone().map(ResponseItemId::from_server), + role: "assistant".to_owned(), + content: vec![ContentItem::OutputText { + annotations: None, + logprobs: None, + text, + }], + // Older Guardian clients exclude commentary from root-to-worker + // review. Keep the phase unspecified so they can review this + // confirmed delivery without claiming it ended the turn. + phase: None, + internal_chat_message_metadata_passthrough: Some( + InternalChatMessageMetadataPassthrough { + turn_id: Some(message.turn_id.clone()), + ..Default::default() + }, + ), + }), + id_allocation: None, + }, metadata: Some(Cow::Owned(CodexHarnessMetadata { delivered_assistant_message: Some(marker), user_input_order: Some(*acceptance_order), @@ -258,7 +298,7 @@ impl From> for RolloutItem { .. }, Some(metadata), - ) = (payload.as_ref(), metadata.as_deref()) + ) = (payload.item.as_ref(), metadata.as_deref()) && role == "assistant" && let [ContentItem::OutputText { text, .. }] = content.as_slice() && let Some(turn_id) = passthrough.turn_id.as_ref() @@ -287,12 +327,18 @@ impl From> for RolloutItem { ); } Self::ResponseItem(ResponseItemEnvelope { - item: payload.into_owned(), + item: payload.into(), metadata: metadata.map(Cow::into_owned), }) } RolloutItemWire::InterAgentCommunication { payload } => { - Self::InterAgentCommunication(payload.into_owned()) + let mut message = payload.item.into_owned(); + if let Some(allocation) = payload.id_allocation + && let Some(id) = message.id.take() + { + message.id = Some(id.with_allocation(allocation.into_owned())); + } + Self::InterAgentCommunication(message) } RolloutItemWire::InterAgentCommunicationMetadata { payload } => { Self::InterAgentCommunicationMetadata { @@ -326,7 +372,7 @@ pub(super) struct InterAgentCommunicationMetadataPayload { pub(super) struct CompactedItemWire<'a> { message: Cow<'a, str>, #[serde(default, skip_serializing_if = "Option::is_none")] - replacement_history: Option>>, + replacement_history: Option>>, #[serde(default, skip_serializing_if = "Option::is_none")] replacement_history_metadata: Option>>, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -373,7 +419,7 @@ impl<'a> From<&'a CompactedItem> for CompactedItemWire<'a> { replacement_history: item.replacement_history.as_ref().map(|items| { items .iter() - .map(|envelope| Cow::Borrowed(&envelope.item)) + .map(|envelope| (&envelope.item).into()) .collect() }), replacement_history_metadata, @@ -415,7 +461,7 @@ impl TryFrom> for CompactedItem { .into_iter() .zip(metadata) .map(|(item, metadata)| ResponseItemEnvelope { - item: item.into_owned(), + item: item.into(), metadata: Some(metadata.into_owned()), }) .collect(), @@ -424,7 +470,7 @@ impl TryFrom> for CompactedItem { (Some(items), None) => Some( items .into_iter() - .map(|item| ResponseItemEnvelope::new(item.into_owned())) + .map(|item| ResponseItemEnvelope::new(item.into())) .collect(), ), (None, Some(_)) => { diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index 3e929d3926..670388dc68 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -274,6 +274,86 @@ fn response_item_rollout_line_preserves_shape() -> Result<()> { Ok(()) } +#[test] +fn rollout_ids_preserve_allocations_but_responses_ids_are_strings() -> Result<()> { + for (rendered, allocation) in [ + ("msg_item042", None), + ( + "msg_item042", + Some(json!({"prefix":"msg","salt":"item","ordinal":9,"value":42})), + ), + ( + "item042_2", + Some(json!({"salt":"item","ordinal":17,"value":42,"suffix":"_2"})), + ), + ( + "provider-output-2", + Some(json!({"salt":"item","ordinal":17,"value":42})), + ), + ] { + let with_allocation = |mut payload: serde_json::Value| { + if let Some(allocation) = &allocation { + payload["id_allocation"] = allocation.clone(); + } + payload + }; + let raw = json!({"type":"message","role":"assistant","content":[],"id":rendered}); + let payload = with_allocation(raw.clone()); + let wire = json!({"type":"response_item","payload":payload}); + let record: RolloutItem = serde_json::from_value(wire.clone())?; + assert_eq!(serde_json::to_value(&record)?, wire); + let RolloutItem::ResponseItem(envelope) = record else { + panic!("expected response item") + }; + let id = envelope.item.id().expect("item ID"); + assert_eq!(id.as_str(), rendered); + assert_eq!(serde_json::to_value(id.allocation())?, json!(allocation)); + assert_eq!(serde_json::to_value(&envelope.item)?, raw); + let caller_item: ResponseItem = serde_json::from_value(payload.clone())?; + assert_eq!(caller_item.id().expect("wire ID").allocation(), None); + + let raw_message = json!({ + "author":"/root","recipient":"/root/child","other_recipients":[], + "content":"hello","trigger_turn":false,"id":rendered + }); + let agent_message = json!({"type":"inter_agent_communication","payload":with_allocation(raw_message.clone())}); + let record: RolloutItem = serde_json::from_value(agent_message.clone())?; + assert_eq!(serde_json::to_value(&record)?, agent_message); + let RolloutItem::InterAgentCommunication(message) = record else { + unreachable!() + }; + assert_eq!(serde_json::to_value(message)?, raw_message); + + let stored_id = match &allocation { + Some(allocation) => json!({"id":rendered,"id_allocation":allocation}), + None => json!(rendered), + }; + let source = json!({ + "id": {"message_id":"message", "turn_id":"turn", "role":"assistant"}, + "revision": stored_id, + "complete": true + }); + let metadata: CodexHarnessMetadata = serde_json::from_value(json!({ + "guardian_review_ids": [stored_id], "retained_source": source, + "guardian_sources": [source] + }))?; + let saved = serde_json::to_value(metadata)?; + assert_eq!(saved["guardian_review_ids"], json!([stored_id])); + assert_eq!(saved["retained_source"], source); + assert_eq!(saved["guardian_sources"], json!([source])); + + let compacted: CompactedItem = serde_json::from_value(json!({ + "message":"summary", + "replacement_history":[payload], + "guardian_history":[payload], + }))?; + let checkpoint = serde_json::to_value(compacted)?; + assert_eq!(&checkpoint["replacement_history"][0], &payload); + assert_eq!(&checkpoint["guardian_history"][0], &payload); + } + Ok(()) +} + #[test] fn delivered_assistant_rollout_survives_an_earlier_read_and_rewrite() -> Result<()> { #[derive(serde::Serialize, serde::Deserialize)] @@ -1004,15 +1084,22 @@ fn rollout_item_schema_matches_tagged_payload_and_sibling_metadata() -> Result<( .find(|variant| variant["properties"]["type"]["enum"] == json!(["response_item"])) .expect("response item schema"); assert!(response_item["properties"].get("metadata").is_some()); - assert_eq!( - response_item["properties"]["payload"]["$ref"], - json!("#/definitions/ResponseItem") - ); + let payload_ref = response_item["properties"]["payload"]["$ref"] + .as_str() + .expect("response item payload reference"); + let payload = schema + .pointer( + payload_ref + .strip_prefix('#') + .expect("local schema reference"), + ) + .expect("response item payload"); + assert!(payload["properties"].get("id_allocation").is_some()); let compacted = &schema["definitions"]["CompactedItem"]; assert_eq!( compacted["properties"]["replacement_history"]["items"]["$ref"], - json!("#/definitions/ResponseItem") + payload_ref ); assert_eq!( compacted["properties"]["replacement_history_metadata"]["items"]["$ref"], @@ -1071,6 +1158,42 @@ fn compacted_item_serializes_window_number_and_id() -> Result<()> { Ok(()) } +#[test] +fn structured_window_ids_round_trip_in_session_metadata_and_compaction() -> Result<()> { + let uuid = "019020ef-10ff-7000-8000-000000000001"; + let meta: codex_protocol::protocol::SessionContextWindow = + serde_json::from_value(json!({"window_id": uuid}))?; + let id = meta.window_id.validated().expect("UUIDv7 window"); + assert_eq!((id.to_string(), id.allocation()), (uuid.to_owned(), None)); + let first = json!({"salt":"window","ordinal":0,"value":42}); + let previous = json!({"salt":"window","ordinal":1,"value":456}); + let current = json!({"salt":"window","ordinal":2,"value":7}); + let header = json!({"window_id": first}); + let meta: codex_protocol::protocol::SessionContextWindow = + serde_json::from_value(header.clone())?; + assert_eq!(meta.window_id.to_string(), "window042"); + assert_eq!(serde_json::to_value(meta)?, header); + let item: CompactedItem = serde_json::from_value(json!({ + "message":"summary", "window_number":2, "first_window_id":first, + "previous_window_id":previous, "window_id":current, + }))?; + let id = item.window_id.as_ref().unwrap().validated().unwrap(); + assert_eq!( + (id.to_string(), id.allocation().unwrap().ordinal()), + ("window007".to_string(), 2) + ); + let wire = serde_json::to_value(item)?; + assert_eq!( + ( + &wire["first_window_id"], + &wire["previous_window_id"], + &wire["window_id"] + ), + (&first, &previous, ¤t) + ); + Ok(()) +} + #[test] /// Keeps legacy numeric window IDs readable in stored compacted items. fn compacted_item_migrates_legacy_numeric_window_id() -> Result<()> { diff --git a/codex-rs/protocol/Cargo.toml b/codex-rs/protocol/Cargo.toml index 8314592a7d..030fdd3ed7 100644 --- a/codex-rs/protocol/Cargo.toml +++ b/codex-rs/protocol/Cargo.toml @@ -22,6 +22,7 @@ codex-http-client = { workspace = true } codex-network-proxy = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-image = { workspace = true } +codex-utils-model-facing-id = { workspace = true } codex-utils-path-uri = { workspace = true } codex-utils-redacted-string = { workspace = true } codex-utils-string = { workspace = true } diff --git a/codex-rs/protocol/src/context_window_id.rs b/codex-rs/protocol/src/context_window_id.rs index 115439ccd7..60550bfe29 100644 --- a/codex-rs/protocol/src/context_window_id.rs +++ b/codex-rs/protocol/src/context_window_id.rs @@ -1,37 +1,57 @@ -//! Context window identity used for compaction, rollout restoration, and request metadata. +//! Rollouts store a UUIDv7 string or a window allocation. Request metadata uses its display form. +use codex_utils_model_facing_id::ModelFacingId; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::fmt; use uuid::Uuid; -#[derive(derive_more::Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[debug("{_0}")] -#[serde(transparent)] -pub struct ContextWindowId(String); +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum ContextWindowId { + Text(String), + Generated(Box), +} impl ContextWindowId { pub fn new_uuid_v7() -> Self { - Self(Uuid::now_v7().to_string()) + Self::Text(Uuid::now_v7().to_string()) } - /// Reads a persisted window ID, ignoring IDs outside the supported format. + pub fn allocation(&self) -> Option<&ModelFacingId> { + match self { + Self::Text(_) => None, + Self::Generated(id) => (id.salt() == "window").then_some(id), + } + } + + /// Parses a UUIDv7 window ID from a rollout string. pub fn parse(value: &str) -> Option { Uuid::parse_str(value) .ok() .filter(|uuid| uuid.get_version_num() == 7) - .map(|uuid| Self(uuid.to_string())) + .map(|uuid| Self::Text(uuid.to_string())) } - /// Validate a deserialized window ID before restoring it. + /// Restore the window without discarding its allocation ordinal. pub fn validated(&self) -> Option { - Self::parse(&self.0) + match self { + Self::Text(text) => Self::parse(text), + Self::Generated(_) => self.allocation().map(|_| self.clone()), + } } } impl fmt::Display for ContextWindowId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(formatter) + match self { + Self::Text(text) => formatter.write_str(text), + Self::Generated(id) => id.fmt(formatter), + } } } + +#[cfg(test)] +#[path = "context_window_id_tests.rs"] +mod tests; diff --git a/codex-rs/protocol/src/context_window_id_tests.rs b/codex-rs/protocol/src/context_window_id_tests.rs new file mode 100644 index 0000000000..b57f50226c --- /dev/null +++ b/codex-rs/protocol/src/context_window_id_tests.rs @@ -0,0 +1,16 @@ +#![cfg(test)] + +use super::ContextWindowId; +use codex_utils_model_facing_id::ModelFacingIdGenerator; +use pretty_assertions::assert_eq; + +#[test] +fn restores_allocations_from_the_window_generator() { + let window = ContextWindowId::Generated(Box::new( + ModelFacingIdGenerator::new(/*seed*/ 0, "window").fetch_next(), + )); + assert_eq!(window.validated(), Some(window)); + + let item = ModelFacingIdGenerator::new(/*seed*/ 0, "item").fetch_next(); + assert_eq!(ContextWindowId::Generated(Box::new(item)).validated(), None); +} diff --git a/codex-rs/protocol/src/lib.rs b/codex-rs/protocol/src/lib.rs index 8c7442499d..3c725f1c5d 100644 --- a/codex-rs/protocol/src/lib.rs +++ b/codex-rs/protocol/src/lib.rs @@ -17,6 +17,7 @@ use item_id_generator::uuid_v7_item_id_generator_for_test; pub use permission_profile_intersection::PermissionIntersectionError; pub use permission_profile_intersection::intersect_effective_permission_profiles; pub use response_item_id::ResponseItemId; +pub use response_item_id::ResponseItemIdAllocation; pub use response_usage::ResponseUsageMetadata; pub use sanitized_git_url::SanitizedGitUrl; pub use session_id::SessionId; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 13f1eb8a1e..dfe2008a5f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -31,6 +31,7 @@ use schemars::JsonSchema; use crate::ResponseItemId; use crate::mcp::CallToolResult; +use crate::response_item_id::OptionalWireItemId; use codex_utils_path_uri::PathUri; mod configuration_update; @@ -1096,7 +1097,8 @@ impl InternalChatMessageMetadataPassthrough { pub enum ResponseItem { AdditionalTools { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, role: String, #[ts(type = "unknown[]")] @@ -1111,7 +1113,8 @@ pub enum ResponseItem { #[ts(optional)] status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, role: String, content: Vec, @@ -1127,7 +1130,8 @@ pub enum ResponseItem { }, AgentMessage { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, author: String, recipient: String, @@ -1138,7 +1142,8 @@ pub enum ResponseItem { }, Reasoning { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, summary: Vec, #[serde(default, skip_serializing_if = "should_serialize_reasoning_content")] @@ -1155,7 +1160,8 @@ pub enum ResponseItem { encrypted_content: Option, /// Legacy id field retained for compatibility with older payloads. #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, /// Set when using the Responses API. call_id: Option, @@ -1173,7 +1179,8 @@ pub enum ResponseItem { #[ts(optional)] status: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, name: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1196,7 +1203,8 @@ pub enum ResponseItem { #[ts(optional)] encrypted_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, call_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1216,7 +1224,8 @@ pub enum ResponseItem { // We keep this behavior centralized in `FunctionCallOutputPayload`. FunctionCallOutput { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -1239,7 +1248,8 @@ pub enum ResponseItem { #[ts(optional)] encrypted_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -1260,7 +1270,8 @@ pub enum ResponseItem { // text or structured content items. CustomToolCallOutput { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, call_id: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1275,7 +1286,8 @@ pub enum ResponseItem { }, ToolSearchOutput { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, call_id: Option, status: String, @@ -1299,7 +1311,8 @@ pub enum ResponseItem { #[ts(optional)] encrypted_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] @@ -1325,7 +1338,8 @@ pub enum ResponseItem { #[ts(optional)] encrypted_content: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, status: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1339,7 +1353,8 @@ pub enum ResponseItem { #[serde(alias = "compaction_summary")] Compaction { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, encrypted_content: String, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -1354,7 +1369,8 @@ pub enum ResponseItem { CompactionTrigger {}, ContextCompaction { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index dedf79e3bc..18c29a465c 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -61,6 +61,7 @@ use crate::plan_tool::UpdatePlanArgs; use crate::request_permissions::RequestPermissionsEvent; use crate::request_permissions::RequestPermissionsResponse; use crate::request_user_input::RequestUserInputResponse; +use crate::response_item_id::OptionalWireItemId; use crate::turn_input::CyberAccessProgram; use crate::turn_input::SuspendTurnOutcome; use crate::turn_input::TurnInputMode; @@ -994,7 +995,8 @@ impl FromStr for ThreadHistoryMode { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema, TS)] pub struct InterAgentCommunication { #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] + #[serde(with = "OptionalWireItemId")] + #[ts(optional, as = "Option")] pub id: Option, pub author: AgentPath, pub recipient: AgentPath, @@ -3450,7 +3452,7 @@ pub enum MultiAgentVersion { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct SessionContextWindow { - /// UUIDv7 identity of this context window. + /// Persisted model-facing window identity. pub window_id: crate::ContextWindowId, } diff --git a/codex-rs/protocol/src/response_item_id.rs b/codex-rs/protocol/src/response_item_id.rs index 18c854e6d8..289091e486 100644 --- a/codex-rs/protocol/src/response_item_id.rs +++ b/codex-rs/protocol/src/response_item_id.rs @@ -1,6 +1,10 @@ +use std::cmp::Ordering; use std::fmt; +use std::hash::Hash; +use std::hash::Hasher; use std::ops::Deref; +use codex_utils_model_facing_id::ModelFacingId; use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; @@ -8,17 +12,85 @@ use ts_rs::TS; /// A Responses API item ID. New IDs require an explicit prefix; deserialization /// remains permissive so legacy rollouts can still be read. -#[derive( - Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, TS, -)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, TS)] +#[serde(untagged)] +pub enum ResponseItemId { + Text(String), + // TypeScript describes the API, where both variants are strings. + #[ts(skip)] + Generated { + id: String, + id_allocation: Box, + }, +} + +/// The generator allocation and the Responses-specific parts of a structured item ID. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct ResponseItemIdAllocation { + /// Item kind, e.g. `msg` in `msg_item042`. Bare request reservations omit it. + #[serde(skip_serializing_if = "Option::is_none")] + pub prefix: Option, + #[serde(flatten)] + pub model_id: ModelFacingId, + /// Provider-assigned tail, including its separator. + #[serde(skip_serializing_if = "Option::is_none")] + pub suffix: Option, +} + +/// Responses and raw item events exchange optional string IDs. +#[derive(Deserialize, JsonSchema)] #[serde(transparent)] -#[schemars(with = "String")] -#[ts(type = "string")] -pub struct ResponseItemId(String); +pub(crate) struct OptionalWireItemId(Option); + +impl OptionalWireItemId { + pub(crate) fn serialize( + id: &Option, + serializer: S, + ) -> Result { + id.as_deref().serialize(serializer) + } + + pub(crate) fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + Ok(::deserialize(deserializer)? + .0 + .map(ResponseItemId::from_server)) + } +} impl ResponseItemId { pub fn with_suffix(prefix: &str, suffix: impl fmt::Display) -> Self { - Self(format!("{prefix}_{suffix}")) + Self::Text(format!("{prefix}_{suffix}")) + } + + /// Keep the allocation that produced this ID while rendering a string on the Responses wire. + pub fn generated(kind: &str, model_id: ModelFacingId) -> Self { + let (prefix, id) = if kind == model_id.salt() { + (None, model_id.to_string()) + } else { + (Some(kind.to_owned()), format!("{kind}_{model_id}")) + }; + Self::Text(id).with_allocation(ResponseItemIdAllocation { + prefix, + model_id, + suffix: None, + }) + } + + /// Restore a known allocation while keeping the exact wire ID. + pub fn with_allocation(self, allocation: ResponseItemIdAllocation) -> Self { + Self::Generated { + id: self.into(), + id_allocation: Box::new(allocation), + } + } + + pub fn allocation(&self) -> Option<&ResponseItemIdAllocation> { + match self { + Self::Text(_) => None, + Self::Generated { id_allocation, .. } => Some(id_allocation), + } } /// Reserved for physical user inputs admitted by the host. Raw item @@ -28,15 +100,17 @@ impl ResponseItemId { } pub fn is_admitted_user_input(&self) -> bool { - self.0.starts_with("msg_input_") + self.starts_with("msg_input_") } pub fn from_server(value: String) -> Self { - Self(value) + Self::Text(value) } pub fn as_str(&self) -> &str { - &self.0 + match self { + Self::Text(id) | Self::Generated { id, .. } => id, + } } pub fn is_prefixed(&self) -> bool { @@ -45,6 +119,29 @@ impl ResponseItemId { } } +// Lookups by a wire ID must also find an item restored with allocation provenance. +impl PartialEq for ResponseItemId { + fn eq(&self, other: &Self) -> bool { + self.as_str() == other.as_str() + } +} +impl Eq for ResponseItemId {} +impl Hash for ResponseItemId { + fn hash(&self, state: &mut H) { + self.as_str().hash(state); + } +} +impl Ord for ResponseItemId { + fn cmp(&self, other: &Self) -> Ordering { + self.as_str().cmp(other.as_str()) + } +} +impl PartialOrd for ResponseItemId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + impl Deref for ResponseItemId { type Target = str; @@ -65,9 +162,20 @@ impl fmt::Display for ResponseItemId { } } +impl fmt::Debug for ResponseItemId { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_tuple("ResponseItemId") + .field(&self.as_str()) + .finish() + } +} + impl From for String { fn from(value: ResponseItemId) -> Self { - value.0 + match value { + ResponseItemId::Text(id) | ResponseItemId::Generated { id, .. } => id, + } } } diff --git a/codex-rs/protocol/src/response_item_id_tests.rs b/codex-rs/protocol/src/response_item_id_tests.rs index 2a7f17d170..2fb74fecbe 100644 --- a/codex-rs/protocol/src/response_item_id_tests.rs +++ b/codex-rs/protocol/src/response_item_id_tests.rs @@ -1,7 +1,9 @@ use pretty_assertions::assert_eq; +use serde_json::json; use uuid::Uuid; use super::ResponseItemId; +use crate::models::ResponseItem; #[test] fn creates_prefixed_uuid_v7_ids() { @@ -37,6 +39,31 @@ fn deserializes_arbitrary_ids_as_strings() { ); } +#[test] +fn structured_ids_round_trip_but_raw_items_only_accept_strings() { + let model_id = serde_json::from_value(json!({"salt":"item","ordinal":7,"value":42})).unwrap(); + let id = ResponseItemId::generated("msg", model_id); + let mut stored = serde_json::to_value(id).unwrap(); + assert_eq!( + stored, + json!({"id":"msg_item042","id_allocation":{ + "prefix":"msg","salt":"item","ordinal":7,"value":42 + }}) + ); + stored["id"] = json!("msg_item042_2"); + stored["id_allocation"]["suffix"] = json!("_2"); + let id: ResponseItemId = serde_json::from_value(stored.clone()).unwrap(); + assert_eq!(id.as_str(), "msg_item042_2"); + assert_eq!(serde_json::to_value(&id).unwrap(), stored); + + let mut raw = json!({"type": "message", "role": "assistant", "content": [], "id": stored}); + assert!(serde_json::from_value::(raw.clone()).is_err()); + raw["id"] = json!(id.as_str()); + let item: ResponseItem = serde_json::from_value(raw.clone()).unwrap(); + assert_eq!(item.id().unwrap().allocation(), None); + assert_eq!(serde_json::to_value(item).unwrap(), raw); +} + #[test] fn recognizes_non_empty_prefix_and_suffix() { for (value, expected) in [