From 0e0ef5d8183c78b6f7de313afb76acc07a56599b Mon Sep 17 00:00:00 2001 From: rka-oai Date: Wed, 12 Aug 2026 19:59:38 +0000 Subject: [PATCH] Track client-authored developer messages in rollout history (#38243) ## What changed - Mark developer messages received from clients with `client_authored` harness metadata when `retain_client_developer_messages` is enabled. - Carry that provenance through pending input and persist it alongside rollout response items without adding it to provider requests or the serialized `TurnInput` payload. - Preserve existing behavior for unannotated response items and when the feature is disabled. ## Testing - Cover client item injection before and during turns, additional application context, rollout replay, image resize notices, and serialization boundaries. GitOrigin-RevId: f385c177732becde732272e4b8440eb91f5c1039 --- .../app-server-exports-stable.json.zst | Bin 132323 -> 132312 bytes .../tests/suite/v2/thread_inject_items.rs | 149 ++++++++++++++++-- codex-rs/core/src/codex_thread.rs | 2 +- codex-rs/core/src/hook_runtime.rs | 2 +- codex-rs/core/src/session/handlers.rs | 1 + codex-rs/core/src/session/inject.rs | 85 +++++++++- codex-rs/core/src/session/input_queue.rs | 69 +++++++- codex-rs/core/src/session/mod.rs | 49 +++--- codex-rs/core/src/session/tests.rs | 14 +- codex-rs/core/tests/suite/image_rollout.rs | 56 ++++++- codex-rs/core/tests/suite/pending_input.rs | 6 +- codex-rs/ext/goal/src/runtime.rs | 2 +- codex-rs/ext/queue/tests/queue_service.rs | 2 +- codex-rs/history/src/lib.rs | 8 +- codex-rs/history/src/tests.rs | 36 +++-- codex-rs/rollout/src/tests.rs | 2 +- 16 files changed, 423 insertions(+), 60 deletions(-) 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 aa091602c36263b01ef9b8eebcaa828327b2cbc9..e4f780c00fa2a9ad7c6c5a57afe0f530898207d7 100644 GIT binary patch delta 16838 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 Fa1Kck6Au6Y delta 16832 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 diff --git a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs index 701d379669..ec8d08ba39 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_inject_items.rs @@ -2,6 +2,8 @@ use anyhow::Context; use anyhow::Result; use app_test_support::MockResponsesConfig; use app_test_support::TestAppServer; +use codex_app_server_protocol::AdditionalContextEntry; +use codex_app_server_protocol::AdditionalContextKind; use codex_app_server_protocol::ThreadInjectItemsParams; use codex_app_server_protocol::ThreadInjectItemsResponse; use codex_app_server_protocol::ThreadStartParams; @@ -10,6 +12,7 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; use codex_core::RolloutRecorder; +use codex_features::Feature; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_rollout::InitialHistory; @@ -18,6 +21,7 @@ use core_test_support::responses; use core_test_support::responses::strip_response_item_id; use core_test_support::responses::strip_response_item_ids_from_json; use serde_json::Value; +use std::collections::HashMap; use tempfile::TempDir; use tokio::time::timeout; @@ -34,7 +38,9 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu let response_mock = responses::mount_sse_once(&server, body).await; let codex_home = TempDir::new()?; - MockResponsesConfig::new(&server.uri()).write(codex_home.path())?; + MockResponsesConfig::new(&server.uri()) + .enable_feature(Feature::RetainClientDeveloperMessages) + .write(codex_home.path())?; let mut mcp = TestAppServer::builder() .with_codex_home(codex_home.path()) @@ -60,11 +66,27 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu phase: None, internal_chat_message_metadata_passthrough: None, }; + let developer_item = |text: &str| ResponseItem::Message { + id: None, + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: text.to_string(), + }], + phase: None, + internal_chat_message_metadata_passthrough: None, + }; + let injected_developer_item = developer_item("Injected developer context"); + let marker_shaped_developer_item = + developer_item("\nclient message\n"); let inject_req = mcp .send_thread_inject_items_request(ThreadInjectItemsParams { thread_id: thread.id.clone(), - items: vec![serde_json::to_value(&injected_item)?], + items: vec![ + serde_json::to_value(&injected_item)?, + serde_json::to_value(&injected_developer_item)?, + serde_json::to_value(&marker_shaped_developer_item)?, + ], }) .await?; let _response: ThreadInjectItemsResponse = @@ -75,14 +97,36 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu let InitialHistory::Resumed(resumed_history) = history else { panic!("expected resumed rollout history"); }; - assert!( - resumed_history - .history - .iter() - .any(|item| matches!(item, RolloutItem::ResponseItem(response_item) if strip_response_item_id(responses::strip_metadata(response_item.item.clone())) == injected_item)), - "injected item should be persisted in rollout history" + let persisted_injected_items = resumed_history + .history + .iter() + .filter_map(|item| match item { + RolloutItem::ResponseItem(envelope) => Some(( + strip_response_item_id(responses::strip_metadata(envelope.item.clone())), + envelope + .metadata + .as_ref() + .map(|metadata| metadata.client_authored), + )), + _ => None, + }) + .filter(|(item, _)| { + item == &injected_item + || item == &injected_developer_item + || item == &marker_shaped_developer_item + }) + .collect::>(); + assert_eq!( + persisted_injected_items, + vec![ + (injected_item.clone(), None), + (injected_developer_item.clone(), Some(true)), + (marker_shaped_developer_item.clone(), Some(true)), + ] ); + let application_context_text = "Application developer context"; + let untrusted_context_text = "Untrusted client context"; let turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), @@ -91,6 +135,22 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu text: "Hello".to_string(), text_elements: Vec::new(), }], + additional_context: Some(HashMap::from([ + ( + "application_context".to_string(), + AdditionalContextEntry { + value: application_context_text.to_string(), + kind: AdditionalContextKind::Application, + }, + ), + ( + "untrusted_context".to_string(), + AdditionalContextEntry { + value: untrusted_context_text.to_string(), + kind: AdditionalContextKind::Untrusted, + }, + ), + ])), ..Default::default() }) .await?; @@ -101,6 +161,44 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu ) .await??; + let InitialHistory::Resumed(resumed_history) = + RolloutRecorder::get_rollout_history(rollout_path).await? + else { + panic!("expected resumed rollout history"); + }; + let persisted_additional_context = resumed_history + .history + .iter() + .filter_map(|item| match item { + RolloutItem::ResponseItem(envelope) => { + let ResponseItem::Message { role, content, .. } = &envelope.item else { + return None; + }; + content.iter().find_map(|item| { + let ContentItem::InputText { text } = item else { + return None; + }; + (text.contains(application_context_text) + || text.contains(untrusted_context_text)) + .then(|| { + ( + role.as_str(), + envelope + .metadata + .as_ref() + .map(|metadata| metadata.client_authored), + ) + }) + }) + } + _ => None, + }) + .collect::>(); + assert_eq!( + persisted_additional_context, + vec![("developer", Some(true)), ("user", None)] + ); + let injected_value = serde_json::to_value(&injected_item)?; let model_input: Vec = response_mock .single_request() @@ -108,6 +206,16 @@ async fn thread_inject_items_adds_raw_response_items_to_thread_history() -> Resu .into_iter() .map(strip_response_item_ids_from_json) .collect(); + assert!( + model_input + .iter() + .all(|item| item.get("metadata").is_none() && item.get("client_authored").is_none()), + "private harness metadata must never enter the provider request" + ); + assert!( + response_item_text_position(&model_input, application_context_text).is_some(), + "application-provided developer context should reach the model" + ); let environment_context_index = response_item_text_position(&model_input, "") .expect("environment context should be injected before the first user turn"); @@ -182,8 +290,8 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() let injected_item = ResponseItem::Message { id: None, - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { + role: "developer".to_string(), + content: vec![ContentItem::InputText { text: "Injected after first turn".to_string(), }], phase: None, @@ -200,6 +308,27 @@ async fn thread_inject_items_adds_raw_response_items_after_a_turn() -> Result<() let _response: ThreadInjectItemsResponse = timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(inject_req)).await??; + let rollout_path = thread.path.as_ref().context("thread path missing")?; + let InitialHistory::Resumed(resumed_history) = + RolloutRecorder::get_rollout_history(rollout_path).await? + else { + panic!("expected resumed rollout history"); + }; + let persisted_developer_item = resumed_history + .history + .iter() + .find_map(|item| match item { + RolloutItem::ResponseItem(envelope) + if strip_response_item_id(responses::strip_metadata(envelope.item.clone())) + == injected_item => + { + Some(envelope) + } + _ => None, + }) + .context("injected developer item should be persisted")?; + assert_eq!(persisted_developer_item.metadata, None); + let second_turn_req = mcp .send_turn_start_request(TurnStartParams { thread_id: thread.id.clone(), diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index 16956a112b..9be1bbac1e 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -570,7 +570,7 @@ impl CodexThread { .await?; } self.session - .inject_no_new_turn(items, Some(turn_context.as_ref())) + .inject_client_response_items(items, turn_context.as_ref()) .await; Ok(()) } diff --git a/codex-rs/core/src/hook_runtime.rs b/codex-rs/core/src/hook_runtime.rs index e3628e5b34..279edbf9d2 100644 --- a/codex-rs/core/src/hook_runtime.rs +++ b/codex-rs/core/src/hook_runtime.rs @@ -590,7 +590,7 @@ pub(crate) async fn record_pending_input( .await; } TurnInput::ResponseItem(item) => { - sess.record_conversation_items(turn_context, std::slice::from_ref(&item)) + sess.record_annotated_conversation_items(turn_context, vec![item]) .await; } TurnInput::InterAgentCommunication(communication) => { diff --git a/codex-rs/core/src/session/handlers.rs b/codex-rs/core/src/session/handlers.rs index 558527e4bf..1edae00513 100644 --- a/codex-rs/core/src/session/handlers.rs +++ b/codex-rs/core/src/session/handlers.rs @@ -277,6 +277,7 @@ pub(super) async fn user_input_or_turn_inner( let mut task_input = additional_context_input .into_iter() .map(ResponseItem::from) + .map(|item| sess.annotate_client_response_item(item)) .map(TurnInput::ResponseItem) .collect::>(); if !items.is_empty() { diff --git a/codex-rs/core/src/session/inject.rs b/codex-rs/core/src/session/inject.rs index 4b3fc7c752..7cb44e717f 100644 --- a/codex-rs/core/src/session/inject.rs +++ b/codex-rs/core/src/session/inject.rs @@ -7,6 +7,9 @@ use crate::state::ActiveTurn; use crate::state::TurnState; use crate::tasks::MailboxParentProvenance; use crate::tasks::RegularTask; +use codex_features::Feature; +use codex_history::CodexHarnessMetadata; +use codex_history::ResponseItemEnvelope; use codex_protocol::config_types::ModeKind; use codex_protocol::models::ResponseItem; use std::sync::Arc; @@ -27,7 +30,11 @@ impl Session { self.input_queue .extend_pending_input_and_accept_mailbox_delivery_for_turn_state( active_turn.turn_state.as_ref(), - input.into_iter().map(TurnInput::ResponseItem).collect(), + input + .into_iter() + .map(ResponseItemEnvelope::new) + .map(TurnInput::ResponseItem) + .collect(), ) .await; Ok(()) @@ -36,6 +43,82 @@ impl Session { } } + /// Preserves trusted client provenance while items wait for an active turn. + #[expect( + clippy::await_holding_invalid_type, + reason = "active turn checks and turn state updates must remain atomic" + )] + pub(crate) async fn inject_client_response_items( + &self, + items: Vec, + turn_context: &TurnContext, + ) { + let items = items + .into_iter() + .map(|item| self.annotate_client_response_item(item)) + .collect::>(); + let mut active = self.active_turn.lock().await; + if let Some(active_turn) = active.as_mut() { + self.input_queue + .extend_pending_input_and_accept_mailbox_delivery_for_turn_state( + active_turn.turn_state.as_ref(), + items.into_iter().map(TurnInput::ResponseItem).collect(), + ) + .await; + return; + } + drop(active); + self.record_annotated_conversation_items(turn_context, items) + .await; + } + + pub(crate) fn annotate_client_response_item(&self, item: ResponseItem) -> ResponseItemEnvelope { + let metadata = (self.enabled(Feature::RetainClientDeveloperMessages) + && matches!(&item, ResponseItem::Message { role, .. } if role == "developer")) + .then_some(CodexHarnessMetadata { + client_authored: true, + }); + + ResponseItemEnvelope { item, metadata } + } + + pub(crate) async fn record_annotated_conversation_items( + &self, + turn_context: &TurnContext, + items: Vec, + ) { + if !self.enabled(Feature::RetainClientDeveloperMessages) + || items.iter().all(|item| item.metadata.is_none()) + { + let items = items + .into_iter() + .map(ResponseItemEnvelope::into_item) + .collect::>(); + self.record_conversation_items(turn_context, &items).await; + return; + } + + let mut annotated_items = Vec::with_capacity(items.len()); + let mut image_preparations = Vec::new(); + for envelope in items { + let (prepared_items, prepared_images) = self.prepare_conversation_items_for_history( + turn_context, + std::slice::from_ref(&envelope.item), + ); + image_preparations.extend(prepared_images); + + let mut metadata = envelope.metadata; + annotated_items.extend(prepared_items.into_owned().into_iter().map(|item| { + ResponseItemEnvelope { + item, + metadata: metadata.take(), + } + })); + } + self.record_prepared_conversation_items(turn_context, annotated_items, image_preparations) + .await; + } + /// Starts a regular turn with the provided input only if automatic idle work /// is allowed for the current session state. /// diff --git a/codex-rs/core/src/session/input_queue.rs b/codex-rs/core/src/session/input_queue.rs index 9b97abf127..1291707155 100644 --- a/codex-rs/core/src/session/input_queue.rs +++ b/codex-rs/core/src/session/input_queue.rs @@ -3,6 +3,7 @@ use crate::state::MailboxDeliveryPhase; use crate::state::TurnState; use codex_diagnostics::Gauge; use codex_diagnostics::GaugeGuard; +use codex_history::ResponseItemEnvelope; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::user_input::UserInput; @@ -22,10 +23,44 @@ pub enum TurnInput { content: Vec, client_id: Option, }, - ResponseItem(ResponseItem), + // Preserve the existing serialized format while carrying injection API metadata + // through the in-memory queue. + ResponseItem(#[serde(with = "turn_input_response_item")] ResponseItemEnvelope), InterAgentCommunication(InterAgentCommunication), } +mod turn_input_response_item { + use super::ResponseItem; + use super::ResponseItemEnvelope; + use serde::Deserialize; + use serde::Deserializer; + use serde::Serialize; + use serde::Serializer; + use serde::ser::Error as _; + + pub(super) fn serialize( + item: &ResponseItemEnvelope, + serializer: S, + ) -> Result + where + S: Serializer, + { + if item.metadata.is_some() { + return Err(S::Error::custom( + "annotated response items cannot cross the turn-input serialization boundary", + )); + } + item.item.serialize(serializer) + } + + pub(super) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + ResponseItem::deserialize(deserializer).map(ResponseItemEnvelope::new) + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum InputQueueActivity { Mailbox, @@ -339,9 +374,41 @@ impl TurnInputQueue { #[cfg(test)] mod tests { use super::*; + use codex_history::CodexHarnessMetadata; use codex_protocol::AgentPath; use pretty_assertions::assert_eq; + #[test] + fn response_item_serde_preserves_legacy_shape_and_rejects_metadata() { + let item = ResponseItem::Other; + let input = TurnInput::ResponseItem(item.clone().into()); + let value = serde_json::json!({"ResponseItem": item}); + + assert_eq!(serde_json::to_value(&input).unwrap(), value); + assert_eq!(serde_json::from_value::(value).unwrap(), input); + + let annotated = TurnInput::ResponseItem(ResponseItemEnvelope { + item: ResponseItem::Other, + metadata: Some(CodexHarnessMetadata { + client_authored: true, + }), + }); + assert!(serde_json::to_value(annotated).is_err()); + + let forged = serde_json::json!({ + "ResponseItem": { + "type": "message", + "role": "developer", + "content": [], + "metadata": {"client_authored": true} + } + }); + let TurnInput::ResponseItem(envelope) = serde_json::from_value(forged).unwrap() else { + panic!("expected response item"); + }; + assert!(envelope.metadata.is_none()); + } + fn make_mail( author: AgentPath, recipient: AgentPath, diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index dd77f8504d..51aa3d6d18 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -3037,14 +3037,33 @@ impl Session { ) { let (items, image_preparations) = self.prepare_conversation_items_for_history(turn_context, items); - let items = items.as_ref(); + let items = items + .into_owned() + .into_iter() + .map(ResponseItemEnvelope::new) + .collect(); + self.record_prepared_conversation_items(turn_context, items, image_preparations) + .await; + } + + async fn record_prepared_conversation_items( + &self, + turn_context: &TurnContext, + items: Vec, + image_preparations: Vec, + ) { + let response_items = items + .iter() + .map(|envelope| envelope.item.clone()) + .collect::>(); { let mut state = self.state.lock().await; - state.current_time_reminder.note_recorded_items(items); - state.record_items( - items.iter(), - turn_context.model_info.truncation_policy.into(), - ); + state + .current_time_reminder + .note_recorded_items(&response_items); + state + .history + .record_annotated_items(&items, turn_context.model_info.truncation_policy.into()); } for image in image_preparations { self.services @@ -3054,8 +3073,11 @@ impl Session { metadata: image, }); } - self.persist_rollout_response_items(items).await; - self.send_raw_response_items(turn_context, items).await; + let rollout_items: Vec = + items.into_iter().map(RolloutItem::ResponseItem).collect(); + self.persist_rollout_items(&rollout_items).await; + self.send_raw_response_items(turn_context, &response_items) + .await; } pub(crate) async fn record_step_world_state_if_changed( @@ -3349,16 +3371,6 @@ impl Session { } } - async fn persist_rollout_response_items(&self, items: &[ResponseItem]) { - let rollout_items: Vec = items - .iter() - .cloned() - .map(ResponseItemEnvelope::new) - .map(RolloutItem::ResponseItem) - .collect(); - self.persist_rollout_items(&rollout_items).await; - } - pub fn enabled(&self, feature: Feature) -> bool { self.features.enabled(feature) } @@ -4086,6 +4098,7 @@ impl Session { let mut pending_input = additional_context_input .into_iter() .map(ResponseItem::from) + .map(|item| self.annotate_client_response_item(item)) .map(TurnInput::ResponseItem) .collect::>(); pending_input.push(TurnInput::UserInput { diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 5f2a3ebdd2..3fe765c75e 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -10636,7 +10636,7 @@ async fn try_start_turn_if_idle_rejects_active_turn_without_injecting() { ) .await; - let item = TurnInput::ResponseItem(user_message("synthetic idle input")); + let item = TurnInput::ResponseItem(user_message("synthetic idle input").into()); let err = sess .try_start_turn_if_idle(vec![item.clone()]) .await @@ -10662,7 +10662,7 @@ async fn try_start_turn_if_idle_rejects_plan_mode_without_injecting() { state.session_configuration.collaboration_mode = collaboration_mode; } - let item = TurnInput::ResponseItem(user_message("synthetic idle input")); + let item = TurnInput::ResponseItem(user_message("synthetic idle input").into()); let err = sess .try_start_turn_if_idle(vec![item.clone()]) .await @@ -10718,7 +10718,7 @@ async fn try_start_turn_if_idle_rejects_empty_user_input_in_plan_mode() { content: Vec::new(), client_id: Some("empty-queued-user-message".to_string()), }, - TurnInput::ResponseItem(user_message("automatic idle input")), + TurnInput::ResponseItem(user_message("automatic idle input").into()), ]; let error = sess .try_start_turn_if_idle(input.clone()) @@ -10747,7 +10747,7 @@ async fn try_start_turn_if_idle_rejects_pending_trigger_turn_without_injecting() ) .await; - let item = TurnInput::ResponseItem(user_message("synthetic idle input")); + let item = TurnInput::ResponseItem(user_message("synthetic idle input").into()); let err = sess .try_start_turn_if_idle(vec![item.clone()]) .await @@ -10775,7 +10775,7 @@ async fn try_start_turn_if_idle_rejects_active_review_turn_without_injecting() { ) .await; - let item = TurnInput::ResponseItem(user_message("synthetic idle input")); + let item = TurnInput::ResponseItem(user_message("synthetic idle input").into()); let err = sess .try_start_turn_if_idle(vec![item.clone()]) .await @@ -10987,7 +10987,7 @@ async fn abort_empty_active_turn_preserves_pending_input() { sess.input_queue .extend_pending_input_for_turn_state( turn_state.as_ref(), - vec![TurnInput::ResponseItem(pending_item.clone())], + vec![TurnInput::ResponseItem(pending_item.clone().into())], ) .await; @@ -10998,7 +10998,7 @@ async fn abort_empty_active_turn_preserves_pending_input() { sess.input_queue .take_pending_input_for_turn_state(turn_state.as_ref()) .await, - vec![TurnInput::ResponseItem(pending_item)] + vec![TurnInput::ResponseItem(pending_item.into())] ); } diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 23bec89cbe..bb39beaab6 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -8,6 +8,7 @@ use codex_protocol::models::ContentItem; use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::ImageDetail; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; @@ -355,6 +356,9 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R let mut resume_builder = test_codex().with_config(|config| { let _ = config.features.enable(Feature::ImageResizeNotice); + let _ = config + .features + .enable(Feature::RetainClientDeveloperMessages); }); let resumed = resume_builder .resume(&server, initial.home.clone(), rollout_path.clone()) @@ -372,7 +376,7 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R .codex .submit(Op::UserInput { items: vec![UserInput::Image { - image_url: original_image_url, + image_url: original_image_url.clone(), detail: Some(ImageDetail::High), }], final_output_json_schema: None, @@ -446,7 +450,7 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R resumed.codex.shutdown_and_wait().await?; let replayed = resume_builder - .resume(&server, resumed.home.clone(), rollout_path) + .resume(&server, resumed.home.clone(), rollout_path.clone()) .await?; let replayed_mock = responses::mount_sse_once( &server, @@ -466,5 +470,53 @@ async fn resumed_history_only_emits_resize_notices_for_new_images() -> anyhow::R .collect::>(); assert_eq!(replayed_notices, vec![expected_notice.to_string()]); + let replayed = test_codex() + .with_config(|config| { + let _ = config.features.enable(Feature::ImageResizeNotice); + let _ = config + .features + .enable(Feature::RetainClientDeveloperMessages); + }) + .restart(&server, &replayed) + .await?; + let existing_rollout_lines = fs::read_to_string(&rollout_path)?.lines().count(); + replayed + .codex + .inject_response_items(vec![ + ResponseInputItem::Message { + role: "user".to_string(), + content: vec![ContentItem::InputImage { + image_url: original_image_url, + detail: Some(ImageDetail::High), + }], + phase: None, + } + .into(), + ResponseInputItem::Message { + role: "developer".to_string(), + content: vec![ContentItem::InputText { + text: "client message".to_string(), + }], + phase: None, + } + .into(), + ]) + .await?; + let persisted_developer_metadata = fs::read_to_string(&rollout_path)? + .lines() + .skip(existing_rollout_lines) + .filter_map(|line| { + let RolloutItem::ResponseItem(envelope) = serde_json::from_str::(line) + .expect("new rollout line should deserialize") + .item + else { + return None; + }; + matches!(envelope.item, ResponseItem::Message { role, .. } if role == "developer") + .then(|| envelope.metadata.map(|metadata| metadata.client_authored)) + }) + .collect::>(); + assert_eq!(persisted_developer_metadata, vec![None, Some(true)]); + Ok(()) } diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 4f6fd96778..9711ed45e4 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -71,9 +71,9 @@ async fn idle_response_items_include_pending_mailbox_in_first_request() -> anyho submit_queue_only_agent_mail(test.codex.as_ref(), "pending mailbox input").await; test.codex - .try_start_turn_if_idle(vec![TurnInput::ResponseItem(responses::user_message_item( - "automatic response item", - ))]) + .try_start_turn_if_idle(vec![TurnInput::ResponseItem( + responses::user_message_item("automatic response item").into(), + )]) .await .map_err(|error| { anyhow::anyhow!("idle response items were rejected: {:?}", error.reason()) diff --git a/codex-rs/ext/goal/src/runtime.rs b/codex-rs/ext/goal/src/runtime.rs index 324e7a23d2..d85cfbeb77 100644 --- a/codex-rs/ext/goal/src/runtime.rs +++ b/codex-rs/ext/goal/src/runtime.rs @@ -404,7 +404,7 @@ impl GoalRuntimeHandle { let item = continuation_steering_item(&protocol_goal_from_state(goal)); if let Err(err) = thread - .try_start_turn_if_idle(vec![TurnInput::ResponseItem(item)]) + .try_start_turn_if_idle(vec![TurnInput::ResponseItem(item.into())]) .await { let reason = err.reason(); diff --git a/codex-rs/ext/queue/tests/queue_service.rs b/codex-rs/ext/queue/tests/queue_service.rs index 4c0f7a7c07..83c4c0cf82 100644 --- a/codex-rs/ext/queue/tests/queue_service.rs +++ b/codex-rs/ext/queue/tests/queue_service.rs @@ -616,7 +616,7 @@ async fn non_user_input_cannot_enter_the_user_message_queue() -> anyhow::Result< let error = service .enqueue( ThreadId::new(), - TurnInput::ResponseItem(ResponseItem::Other), + TurnInput::ResponseItem(ResponseItem::Other.into()), ) .await .expect_err("response item should not enter the user queue"); diff --git a/codex-rs/history/src/lib.rs b/codex-rs/history/src/lib.rs index 493841fd98..26788c95d0 100644 --- a/codex-rs/history/src/lib.rs +++ b/codex-rs/history/src/lib.rs @@ -40,10 +40,12 @@ pub struct ResponseItemEnvelope { /// Metadata owned by the Codex harness and persisted with a response item. /// -/// This intentionally has no fields yet. Keeping it closed prevents rollout -/// metadata from becoming an untyped extension point. #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)] -pub struct CodexHarnessMetadata {} +pub struct CodexHarnessMetadata { + /// Whether a developer message was supplied by an app-server client. + #[serde(default)] + pub client_authored: bool, +} impl ResponseItemEnvelope { /// Wraps a raw Responses API item for persisted history. diff --git a/codex-rs/history/src/tests.rs b/codex-rs/history/src/tests.rs index 5db3d27f4b..b968d6ea7c 100644 --- a/codex-rs/history/src/tests.rs +++ b/codex-rs/history/src/tests.rs @@ -70,7 +70,9 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> ordinal: Some(7), item: RolloutItem::ResponseItem(ResponseItemEnvelope { item: response_item.clone(), - metadata: Some(CodexHarnessMetadata {}), + metadata: Some(CodexHarnessMetadata { + client_authored: true, + }), }), }; let serialized = serde_json::to_value(&line)?; @@ -82,7 +84,7 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> "ordinal": 7, "type": "response_item", "payload": response_item, - "metadata": {}, + "metadata": { "client_authored": true }, }) ); assert_eq!(serialized["payload"].get("metadata"), None); @@ -91,7 +93,12 @@ fn response_item_envelope_stores_metadata_beside_rollout_payload() -> Result<()> let RolloutItem::ResponseItem(envelope) = restored.item else { panic!("expected response item"); }; - assert_eq!(envelope.metadata, Some(CodexHarnessMetadata {})); + assert_eq!( + envelope.metadata, + Some(CodexHarnessMetadata { + client_authored: true, + }) + ); Ok(()) } @@ -118,7 +125,7 @@ fn response_item_envelope_ignores_unknown_harness_metadata_fields() -> Result<() let RolloutItem::ResponseItem(envelope) = line.item else { panic!("expected response item"); }; - assert_eq!(envelope.metadata, Some(CodexHarnessMetadata {})); + assert_eq!(envelope.metadata, Some(CodexHarnessMetadata::default())); let compacted = serde_json::from_value::(json!({ "message": "summary", @@ -127,7 +134,7 @@ fn response_item_envelope_ignores_unknown_harness_metadata_fields() -> Result<() }))?; assert_eq!( compacted.replacement_history.expect("replacement history")[0].metadata, - Some(CodexHarnessMetadata {}) + Some(CodexHarnessMetadata::default()) ); Ok(()) } @@ -175,7 +182,9 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu replacement_history: Some(vec![ ResponseItemEnvelope { item: developer_message.clone(), - metadata: Some(CodexHarnessMetadata {}), + metadata: Some(CodexHarnessMetadata { + client_authored: true, + }), }, ResponseItemEnvelope::new(compaction_item.clone()), ]), @@ -191,7 +200,10 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu json!({ "message": "summary", "replacement_history": [developer_message, compaction_item], - "replacement_history_metadata": [{}, {}], + "replacement_history_metadata": [ + { "client_authored": true }, + { "client_authored": false }, + ], }) ); @@ -201,11 +213,13 @@ fn compacted_replacement_history_stores_metadata_in_an_aligned_sidecar() -> Resu Some(vec![ ResponseItemEnvelope { item: developer_message, - metadata: Some(CodexHarnessMetadata {}), + metadata: Some(CodexHarnessMetadata { + client_authored: true, + }), }, ResponseItemEnvelope { item: compaction_item, - metadata: Some(CodexHarnessMetadata {}), + metadata: Some(CodexHarnessMetadata::default()), }, ]) ); @@ -260,7 +274,9 @@ fn compacted_metadata_remains_compatible_with_legacy_response_item_readers() -> let response_item = response_message("developer"); let envelope = ResponseItemEnvelope { item: response_item.clone(), - metadata: Some(CodexHarnessMetadata {}), + metadata: Some(CodexHarnessMetadata { + client_authored: true, + }), }; let response_line = serde_json::to_value(RolloutItem::ResponseItem(envelope.clone()))?; let LegacyRolloutItem::ResponseItem(legacy_response) = diff --git a/codex-rs/rollout/src/tests.rs b/codex-rs/rollout/src/tests.rs index 7292499451..deb3f0e04b 100644 --- a/codex-rs/rollout/src/tests.rs +++ b/codex-rs/rollout/src/tests.rs @@ -1331,7 +1331,7 @@ async fn read_head_for_summary_omits_harness_metadata() { ordinal: None, item: RolloutItem::ResponseItem(ResponseItemEnvelope { item: response_item.clone(), - metadata: Some(CodexHarnessMetadata {}), + metadata: Some(CodexHarnessMetadata::default()), }), }; fs::write(