From f6f0b8efca3e67cbaa176bba5da99caa02a42ba7 Mon Sep 17 00:00:00 2001 From: pakrym-oai Date: Sat, 20 Jun 2026 23:29:49 -0700 Subject: [PATCH] inline model instructions in initial context --- codex-rs/codex-api/src/common.rs | 12 ++-- .../src/endpoint/responses_websocket.rs | 2 +- codex-rs/codex-api/tests/clients.rs | 6 +- codex-rs/core/src/client.rs | 9 ++- codex-rs/core/src/client_common.rs | 5 +- codex-rs/core/src/client_common_tests.rs | 8 +-- codex-rs/core/src/client_tests.rs | 56 +++++++++++++++++++ codex-rs/core/src/compact.rs | 3 +- codex-rs/core/src/compact_remote.rs | 8 +-- codex-rs/core/src/compact_remote_v2.rs | 7 ++- .../src/context/environment_context_tests.rs | 1 + codex-rs/core/src/context/mod.rs | 2 + .../core/src/context/model_instructions.rs | 32 +++++++++++ codex-rs/core/src/context_manager/history.rs | 21 ++++++- .../core/src/context_manager/history_tests.rs | 1 + codex-rs/core/src/session/mod.rs | 13 ++++- codex-rs/core/src/session/review.rs | 1 + .../session/rollout_reconstruction_tests.rs | 8 +++ codex-rs/core/src/session/tests.rs | 54 +++++++++++++++++- codex-rs/core/src/session/turn.rs | 2 +- codex-rs/core/src/session/turn_context.rs | 8 +++ .../core/tests/common/context_snapshot.rs | 43 ++++++++++++-- codex-rs/core/tests/suite/auto_review.rs | 6 +- codex-rs/core/tests/suite/cli_stream.rs | 16 +----- codex-rs/core/tests/suite/client.rs | 43 +++++++------- .../core/tests/suite/client_websockets.rs | 4 +- codex-rs/core/tests/suite/compact.rs | 49 ++++++++-------- codex-rs/core/tests/suite/compact_remote.rs | 42 +++++++++++--- .../core/tests/suite/mcp_turn_metadata.rs | 5 +- .../core/tests/suite/model_visible_layout.rs | 39 ++++++++++++- codex-rs/core/tests/suite/personality.rs | 8 +-- codex-rs/core/tests/suite/prompt_caching.rs | 55 ++++++++++-------- codex-rs/core/tests/suite/remote_models.rs | 12 +++- codex-rs/core/tests/suite/resume.rs | 15 +++-- codex-rs/core/tests/suite/resume_warning.rs | 1 + codex-rs/core/tests/suite/review.rs | 12 +++- ...text__additional_context_simple_input.snap | 6 +- ...t__manual_compact_with_history_shapes.snap | 8 ++- ...nual_compact_without_prev_user_shapes.snap | 4 +- ...__compact__mid_turn_compaction_shapes.snap | 8 ++- ...mpling_model_switch_compaction_shapes.snap | 15 +++-- ...action_context_window_exceeded_shapes.snap | 4 +- ..._compaction_including_incoming_shapes.snap | 8 ++- ...n_strips_incoming_model_switch_shapes.snap | 17 ++++-- ...t_resume_restates_realtime_end_shapes.snap | 14 +++-- ...ompact_restates_realtime_start_shapes.snap | 14 +++-- ...te_manual_compact_with_history_shapes.snap | 8 ++- ...nual_compact_without_prev_user_shapes.snap | 4 +- ..._does_not_restate_realtime_end_shapes.snap | 18 +++--- ...y_reinjects_above_last_summary_shapes.snap | 11 +++- ...te__remote_mid_turn_compaction_shapes.snap | 8 ++- ...summary_only_reinjects_context_shapes.snap | 8 ++- ...action_context_window_exceeded_shapes.snap | 4 +- ...te_pre_turn_compaction_failure_shapes.snap | 4 +- ..._compaction_including_incoming_shapes.snap | 8 ++- ...mpaction_restates_realtime_end_shapes.snap | 14 +++-- ...action_restates_realtime_start_shapes.snap | 14 +++-- ...n_strips_incoming_model_switch_shapes.snap | 17 ++++-- ...k_followup_turn_trims_context_updates.snap | 8 ++- ...fork__rollback_past_compaction_shapes.snap | 12 +++- ...ut_cwd_change_does_not_refresh_agents.snap | 14 +++-- ...resume_override_matches_rollout_model.snap | 14 +++-- ...layout_resume_with_personality_change.snap | 12 ++-- ...__model_visible_layout_turn_overrides.snap | 14 +++-- codex-rs/protocol/src/protocol.rs | 7 +++ codex-rs/rollout/src/recorder_tests.rs | 1 + codex-rs/state/src/extract.rs | 4 ++ 67 files changed, 654 insertions(+), 257 deletions(-) create mode 100644 codex-rs/core/src/context/model_instructions.rs diff --git a/codex-rs/codex-api/src/common.rs b/codex-rs/codex-api/src/common.rs index 17753e799a..59b8989237 100644 --- a/codex-rs/codex-api/src/common.rs +++ b/codex-rs/codex-api/src/common.rs @@ -26,8 +26,8 @@ pub const WS_REQUEST_HEADER_TRACESTATE_CLIENT_METADATA_KEY: &str = "ws_request_h pub struct CompactionInput<'a> { pub model: &'a str, pub input: &'a [ResponseItem], - #[serde(skip_serializing_if = "str::is_empty")] - pub instructions: &'a str, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option<&'a str>, pub tools: Vec, pub parallel_tool_calls: bool, #[serde(skip_serializing_if = "Option::is_none")] @@ -182,8 +182,8 @@ impl From for OpenAiVerbosity { #[derive(Debug, Serialize, Clone, PartialEq)] pub struct ResponsesApiRequest { pub model: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub instructions: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option, pub input: Vec, pub tools: Vec, pub tool_choice: String, @@ -228,8 +228,8 @@ impl From<&ResponsesApiRequest> for ResponseCreateWsRequest { #[derive(Debug, Serialize)] pub struct ResponseCreateWsRequest { pub model: String, - #[serde(skip_serializing_if = "String::is_empty")] - pub instructions: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub instructions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub previous_response_id: Option, pub input: Vec, diff --git a/codex-rs/codex-api/src/endpoint/responses_websocket.rs b/codex-rs/codex-api/src/endpoint/responses_websocket.rs index 4ea564f139..64ccfd7e2a 100644 --- a/codex-rs/codex-api/src/endpoint/responses_websocket.rs +++ b/codex-rs/codex-api/src/endpoint/responses_websocket.rs @@ -806,7 +806,7 @@ mod tests { fn direct_serialization_preserves_websocket_request_payload() { let request = ResponsesWsRequest::ResponseCreate(ResponseCreateWsRequest { model: "gpt-test".to_string(), - instructions: "Use the available tools.".to_string(), + instructions: Some("Use the available tools.".to_string()), previous_response_id: Some("resp-1".to_string()), input: vec![ResponseItem::Message { id: Some("msg-1".to_string()), diff --git a/codex-rs/codex-api/tests/clients.rs b/codex-rs/codex-api/tests/clients.rs index 488f6dc674..ad9b9c770b 100644 --- a/codex-rs/codex-api/tests/clients.rs +++ b/codex-rs/codex-api/tests/clients.rs @@ -307,7 +307,7 @@ async fn responses_client_stream_request_preserves_item_ids() -> Result<()> { let client = ResponsesClient::new(transport, provider("openai"), Arc::new(NoAuth)); let request = ResponsesApiRequest { model: "gpt-test".into(), - instructions: "Say hi".into(), + instructions: Some("Say hi".into()), input: vec![ResponseItem::Message { id: Some("msg_1".into()), role: "user".into(), @@ -399,7 +399,7 @@ async fn streaming_client_retries_on_transport_error() -> Result<()> { let request = ResponsesApiRequest { model: "gpt-test".into(), - instructions: "Say hi".into(), + instructions: Some("Say hi".into()), input: Vec::new(), tools: Vec::new(), tool_choice: "auto".into(), @@ -512,7 +512,7 @@ async fn azure_store_sends_ids_and_headers() -> Result<()> { let request = ResponsesApiRequest { model: "gpt-test".into(), - instructions: "Say hi".into(), + instructions: Some("Say hi".into()), input: vec![ResponseItem::Message { id: Some("msg_1".into()), role: "user".into(), diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index 73d7398a3d..e1421bf59c 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -535,7 +535,7 @@ impl ModelClient { let payload = ApiCompactionInput { model: &model, input: &input, - instructions: &instructions, + instructions: instructions.as_deref(), tools, parallel_tool_calls, reasoning, @@ -778,7 +778,10 @@ impl ModelClient { service_tier: Option, responses_metadata: &CodexResponsesMetadata, ) -> Result { - let instructions = &prompt.base_instructions.text; + let instructions = prompt + .base_instructions + .as_ref() + .map(|instructions| instructions.text.clone()); let mut input = prompt.get_formatted_input_for_request(model_info.use_responses_lite); if !self.state.provider.info().is_openai() { input.iter_mut().for_each(ResponseItem::clear_metadata); @@ -810,7 +813,7 @@ impl ModelClient { let service_tier = model_info.service_tier_for_request(service_tier); let request = ResponsesApiRequest { model: model_info.slug.clone(), - instructions: instructions.clone(), + instructions, input, tools, tool_choice: "auto".to_string(), diff --git a/codex-rs/core/src/client_common.rs b/codex-rs/core/src/client_common.rs index e0c2ff03c3..f7b51b90de 100644 --- a/codex-rs/core/src/client_common.rs +++ b/codex-rs/core/src/client_common.rs @@ -26,7 +26,8 @@ pub struct Prompt { /// Whether parallel tool calls are permitted for this prompt. pub(crate) parallel_tool_calls: bool, - pub base_instructions: BaseInstructions, + /// Base instructions to send through the top-level Responses API field. + pub base_instructions: Option, /// Optional the output schema for the model's response. pub output_schema: Option, @@ -41,7 +42,7 @@ impl Default for Prompt { input: Vec::new(), tools: Vec::new(), parallel_tool_calls: false, - base_instructions: BaseInstructions::default(), + base_instructions: None, output_schema: None, output_schema_strict: true, } diff --git a/codex-rs/core/src/client_common_tests.rs b/codex-rs/core/src/client_common_tests.rs index 8107049ab3..7703b534a6 100644 --- a/codex-rs/core/src/client_common_tests.rs +++ b/codex-rs/core/src/client_common_tests.rs @@ -108,7 +108,7 @@ fn serializes_text_verbosity_when_set() { let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), - instructions: "i".to_string(), + instructions: Some("i".to_string()), input, tools, tool_choice: "auto".to_string(), @@ -155,7 +155,7 @@ fn serializes_text_schema_with_strict_format() { let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), - instructions: "i".to_string(), + instructions: Some("i".to_string()), input, tools, tool_choice: "auto".to_string(), @@ -216,7 +216,7 @@ fn omits_text_when_not_set() { let tools: Vec = vec![]; let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), - instructions: "i".to_string(), + instructions: Some("i".to_string()), input, tools, tool_choice: "auto".to_string(), @@ -239,7 +239,7 @@ fn omits_text_when_not_set() { fn serializes_flex_service_tier_when_set() { let req = ResponsesApiRequest { model: "gpt-5.4".to_string(), - instructions: "i".to_string(), + instructions: Some("i".to_string()), input: vec![], tools: vec![], tool_choice: "auto".to_string(), diff --git a/codex-rs/core/src/client_tests.rs b/codex-rs/core/src/client_tests.rs index 1d0c4c48c2..5885b18325 100644 --- a/codex-rs/core/src/client_tests.rs +++ b/codex-rs/core/src/client_tests.rs @@ -1,6 +1,7 @@ use super::AuthRequestTelemetryContext; use super::ModelClient; use super::PendingUnauthorizedRetry; +use super::ReasoningSummaryConfig; use super::UnauthorizedRecoveryExecution; use super::X_CODEX_INSTALLATION_ID_HEADER; use super::X_CODEX_PARENT_THREAD_ID_HEADER; @@ -10,6 +11,7 @@ use super::X_OPENAI_SUBAGENT_HEADER; use crate::AttestationContext; use crate::AttestationProvider; use crate::GenerateAttestationFuture; +use crate::client_common::Prompt; use crate::responses_metadata::CodexResponsesMetadata; use crate::test_support::TestCodexResponsesRequestKind; use crate::test_support::responses_metadata as test_responses_metadata; @@ -25,6 +27,7 @@ use codex_model_provider_info::WireApi; use codex_model_provider_info::create_oss_provider_with_base_url; use codex_otel::SessionTelemetry; use codex_protocol::ThreadId; +use codex_protocol::models::BaseInstructions; use codex_protocol::models::ContentItem; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::ModelInfo; @@ -146,6 +149,59 @@ fn test_session_telemetry() -> SessionTelemetry { ) } +#[tokio::test] +async fn response_request_uses_top_level_instructions_only_for_legacy_prompts() { + let client = test_model_client(SessionSource::Exec); + let client_setup = client + .current_client_setup() + .await + .expect("resolve client setup"); + let model_info = test_model_info(); + let responses_metadata = test_responses_metadata_for_client( + &client, + Some("turn-1"), + format!("{}:0", client.state.thread_id), + /*parent_thread_id*/ None, + TestCodexResponsesRequestKind::Turn, + ); + let mut prompt = Prompt { + base_instructions: Some(BaseInstructions { + text: "legacy instructions".to_string(), + }), + ..Default::default() + }; + + let legacy_request = client + .build_responses_request( + &client_setup.api_provider, + &prompt, + &model_info, + /*effort*/ None, + ReasoningSummaryConfig::Auto, + /*service_tier*/ None, + &responses_metadata, + ) + .expect("build legacy request"); + assert_eq!( + legacy_request.instructions, + Some("legacy instructions".to_string()) + ); + + prompt.base_instructions = None; + let inline_request = client + .build_responses_request( + &client_setup.api_provider, + &prompt, + &model_info, + /*effort*/ None, + ReasoningSummaryConfig::Auto, + /*service_tier*/ None, + &responses_metadata, + ) + .expect("build inline request"); + assert_eq!(inline_request.instructions, None); +} + #[derive(Default)] struct TagCollectorVisitor { tags: BTreeMap, diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 8a5efd5947..f45f84704f 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -238,7 +238,8 @@ async fn run_compact_task_inner_impl( let turn_input_len = turn_input.len(); let prompt = Prompt { input: turn_input, - base_instructions: sess.get_base_instructions().await, + base_instructions: (!turn_context.inline_instructions) + .then_some(sess.get_base_instructions().await), ..Default::default() }; let attempt_result = drain_to_completed( diff --git a/codex-rs/core/src/compact_remote.rs b/codex-rs/core/src/compact_remote.rs index 7a47881e47..3aed03fdc2 100644 --- a/codex-rs/core/src/compact_remote.rs +++ b/codex-rs/core/src/compact_remote.rs @@ -227,7 +227,7 @@ async fn run_remote_compact_task_inner_impl( input: prompt_input, tools: tool_router.model_visible_specs(), parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls, - base_instructions, + base_instructions: (!turn_context.inline_instructions).then_some(base_instructions), output_schema: None, output_schema_strict: true, }; @@ -379,8 +379,8 @@ pub(crate) fn trim_function_call_history_to_fit_context_window( let item_count = history.raw_items().len(); for index in (0..item_count).rev() { - let Some(estimated_tokens_before) = - history.estimate_token_count_with_base_instructions(base_instructions) + let Some(estimated_tokens_before) = history + .estimate_token_count_with_resolved_base_instructions(turn_context, base_instructions) else { break; }; @@ -398,7 +398,7 @@ pub(crate) fn trim_function_call_history_to_fit_context_window( items[index] = rewritten_item; history.replace(items); let estimated_tokens_after = history - .estimate_token_count_with_base_instructions(base_instructions) + .estimate_token_count_with_resolved_base_instructions(turn_context, base_instructions) .unwrap_or_default(); rewritten_outputs += 1; estimated_deleted_tokens = estimated_deleted_tokens diff --git a/codex-rs/core/src/compact_remote_v2.rs b/codex-rs/core/src/compact_remote_v2.rs index 0b382700ac..b2d6721d8b 100644 --- a/codex-rs/core/src/compact_remote_v2.rs +++ b/codex-rs/core/src/compact_remote_v2.rs @@ -239,7 +239,7 @@ async fn run_remote_compact_task_inner_impl( input, tools: tool_router.model_visible_specs(), parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls, - base_instructions, + base_instructions: (!turn_context.inline_instructions).then_some(base_instructions), output_schema: None, output_schema_strict: true, }; @@ -252,7 +252,10 @@ async fn run_remote_compact_task_inner_impl( ); let trace_attempt = compaction_trace.start_attempt(&serde_json::json!({ "model": turn_context.model_info.slug.as_str(), - "instructions": prompt.base_instructions.text.as_str(), + "instructions": prompt + .base_instructions + .as_ref() + .map(|instructions| instructions.text.as_str()), "input": &prompt.input, "parallel_tool_calls": prompt.parallel_tool_calls, })); diff --git a/codex-rs/core/src/context/environment_context_tests.rs b/codex-rs/core/src/context/environment_context_tests.rs index d40ce4e671..d442738d08 100644 --- a/codex-rs/core/src/context/environment_context_tests.rs +++ b/codex-rs/core/src/context/environment_context_tests.rs @@ -190,6 +190,7 @@ fn turn_context_item_filesystem_uses_workspace_roots_instead_of_cwd() { let repo_private = repo.join("private"); let item = TurnContextItem { turn_id: None, + inline_instructions: false, cwd: test_abs_path("/not-the-workspace"), workspace_roots: Some(vec![repo.clone(), other_repo.clone()]), current_date: None, diff --git a/codex-rs/core/src/context/mod.rs b/codex-rs/core/src/context/mod.rs index c9319ebef0..68b386ed40 100644 --- a/codex-rs/core/src/context/mod.rs +++ b/codex-rs/core/src/context/mod.rs @@ -16,6 +16,7 @@ mod internal_model_context; mod legacy_apply_patch_exec_command_warning; mod legacy_model_mismatch_warning; mod legacy_unified_exec_process_limit_warning; +mod model_instructions; mod model_switch_instructions; mod multi_agent_mode_instructions; mod network_rule_saved; @@ -59,6 +60,7 @@ pub use internal_model_context::InvalidInternalContextSource; pub(crate) use legacy_apply_patch_exec_command_warning::LegacyApplyPatchExecCommandWarning; pub(crate) use legacy_model_mismatch_warning::LegacyModelMismatchWarning; pub(crate) use legacy_unified_exec_process_limit_warning::LegacyUnifiedExecProcessLimitWarning; +pub(crate) use model_instructions::ModelInstructions; pub(crate) use model_switch_instructions::ModelSwitchInstructions; pub(crate) use multi_agent_mode_instructions::MultiAgentModeInstructions; pub(crate) use network_rule_saved::NetworkRuleSaved; diff --git a/codex-rs/core/src/context/model_instructions.rs b/codex-rs/core/src/context/model_instructions.rs new file mode 100644 index 0000000000..55667f6580 --- /dev/null +++ b/codex-rs/core/src/context/model_instructions.rs @@ -0,0 +1,32 @@ +use super::ContextualUserFragment; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ModelInstructions { + instructions: String, +} + +impl ModelInstructions { + pub(crate) fn new(instructions: impl Into) -> Self { + Self { + instructions: instructions.into(), + } + } +} + +impl ContextualUserFragment for ModelInstructions { + fn role(&self) -> &'static str { + "developer" + } + + fn markers(&self) -> (&'static str, &'static str) { + Self::type_markers() + } + + fn type_markers() -> (&'static str, &'static str) { + ("", "") + } + + fn body(&self) -> String { + self.instructions.clone() + } +} diff --git a/codex-rs/core/src/context_manager/history.rs b/codex-rs/core/src/context_manager/history.rs index 6ce1c6148f..b6306a5c25 100644 --- a/codex-rs/core/src/context_manager/history.rs +++ b/codex-rs/core/src/context_manager/history.rs @@ -135,7 +135,19 @@ impl ContextManager { let base_instructions = BaseInstructions { text: model_info.get_model_instructions(personality), }; - self.estimate_token_count_with_base_instructions(&base_instructions) + self.estimate_token_count_with_resolved_base_instructions(turn_context, &base_instructions) + } + + pub(crate) fn estimate_token_count_with_resolved_base_instructions( + &self, + turn_context: &TurnContext, + base_instructions: &BaseInstructions, + ) -> Option { + if turn_context.inline_instructions { + self.estimate_token_count_without_base_instructions() + } else { + self.estimate_token_count_with_base_instructions(base_instructions) + } } pub(crate) fn estimate_token_count_with_base_instructions( @@ -145,13 +157,18 @@ impl ContextManager { let base_tokens = i64::try_from(approx_token_count(&base_instructions.text)).unwrap_or(i64::MAX); + self.estimate_token_count_without_base_instructions() + .map(|items_tokens| base_tokens.saturating_add(items_tokens)) + } + + fn estimate_token_count_without_base_instructions(&self) -> Option { let items_tokens = self .items .iter() .map(estimate_item_token_count) .fold(0i64, i64::saturating_add); - Some(base_tokens.saturating_add(items_tokens)) + Some(items_tokens) } pub(crate) fn remove_first_item(&mut self) { diff --git a/codex-rs/core/src/context_manager/history_tests.rs b/codex-rs/core/src/context_manager/history_tests.rs index 35a9656fe1..f7d18b4b04 100644 --- a/codex-rs/core/src/context_manager/history_tests.rs +++ b/codex-rs/core/src/context_manager/history_tests.rs @@ -127,6 +127,7 @@ fn developer_msg_with_fragments(texts: &[&str]) -> ResponseItem { fn reference_context_item() -> TurnContextItem { TurnContextItem { turn_id: Some("reference-turn".to_string()), + inline_instructions: false, cwd: AbsolutePathBuf::try_from( std::env::current_dir() .expect("current directory") diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index c85898555b..bc3ec10623 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -27,6 +27,7 @@ use crate::context::AvailablePluginsInstructions; use crate::context::AvailableSkillsInstructions; use crate::context::CollaborationModeInstructions; use crate::context::ContextualUserFragment; +use crate::context::ModelInstructions; use crate::context::MultiAgentModeInstructions; use crate::context::NetworkRuleSaved; use crate::context::PermissionsInstructions; @@ -1403,7 +1404,10 @@ impl Session { ) { let history = self.clone_history().await; let base_instructions = self.get_base_instructions().await; - history.estimate_token_count_with_base_instructions(&base_instructions) + history.estimate_token_count_with_resolved_base_instructions( + turn_context, + &base_instructions, + ) } else { None }; @@ -3034,6 +3038,9 @@ impl Session { state.auto_compact_window_ids(), ) }; + if turn_context.inline_instructions { + developer_sections.push(ModelInstructions::new(base_instructions.clone()).render()); + } if let Some(model_switch_message) = crate::context_manager::updates::build_model_instructions_update_item( previous_turn_settings.as_ref(), @@ -3503,8 +3510,8 @@ impl Session { pub(crate) async fn recompute_token_usage(&self, turn_context: &TurnContext) { let history = self.clone_history().await; let base_instructions = self.get_base_instructions().await; - let Some(estimated_total_tokens) = - history.estimate_token_count_with_base_instructions(&base_instructions) + let Some(estimated_total_tokens) = history + .estimate_token_count_with_resolved_base_instructions(turn_context, &base_instructions) else { return; }; diff --git a/codex-rs/core/src/session/review.rs b/codex-rs/core/src/session/review.rs index 7fccc4d19f..e82fd8ec70 100644 --- a/codex-rs/core/src/session/review.rs +++ b/codex-rs/core/src/session/review.rs @@ -105,6 +105,7 @@ pub(super) async fn spawn_review_thread( sub_id: review_turn_id.clone(), trace_id: current_span_trace_id(), realtime_active: parent_turn_context.realtime_active, + inline_instructions: parent_turn_context.inline_instructions, config: per_turn_config, auth_manager: auth_manager_for_context, model_info: model_info.clone(), diff --git a/codex-rs/core/src/session/rollout_reconstruction_tests.rs b/codex-rs/core/src/session/rollout_reconstruction_tests.rs index 1823b28fdb..e1f5b71f9b 100644 --- a/codex-rs/core/src/session/rollout_reconstruction_tests.rs +++ b/codex-rs/core/src/session/rollout_reconstruction_tests.rs @@ -87,6 +87,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -128,6 +129,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif let previous_model = "previous-rollout-model"; let mut previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -1002,6 +1004,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -1088,6 +1091,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis .expect("serialize seeded reference context item"), serde_json::to_value(Some(TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -1119,6 +1123,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -1245,6 +1250,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo let unmatched_abort_turn_id = "other-turn".to_string(); let current_context_item = TurnContextItem { turn_id: Some(current_turn_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -1366,6 +1372,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, @@ -1533,6 +1540,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear let previous_model = "previous-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 6f84da3c79..cf3debfb74 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1894,6 +1894,56 @@ async fn record_initial_history_new_defers_initial_context_until_first_turn() { assert_eq!(session.previous_turn_settings().await, None); } +#[tokio::test] +async fn build_initial_context_inlines_base_instructions_for_new_turn_contexts() { + let (session, mut turn_context) = make_session_and_context().await; + let base_instructions = "INLINE_BASE_INSTRUCTIONS_MARKER".to_string(); + session + .state + .lock() + .await + .session_configuration + .base_instructions = base_instructions.clone(); + + assert!(turn_context.inline_instructions); + let initial_context = session.build_initial_context(&turn_context).await; + assert!( + developer_input_texts(&initial_context) + .iter() + .any(|text| text.contains(&base_instructions)) + ); + + turn_context.inline_instructions = false; + let legacy_initial_context = session.build_initial_context(&turn_context).await; + assert!( + developer_input_texts(&legacy_initial_context) + .iter() + .all(|text| !text.contains(&base_instructions)) + ); +} + +#[tokio::test] +async fn new_turn_context_inherits_inline_instructions_from_reference_context() { + let (session, turn_context) = make_session_and_context().await; + assert!(turn_context.inline_instructions); + + let mut legacy_context_item = turn_context.to_turn_context_item(); + legacy_context_item.inline_instructions = false; + session + .state + .lock() + .await + .set_reference_context_item(Some(legacy_context_item)); + + let resumed_turn_context = session.new_default_turn().await; + assert!(!resumed_turn_context.inline_instructions); + assert!( + !resumed_turn_context + .to_turn_context_item() + .inline_instructions + ); +} + fn session_meta_item( thread_id: ThreadId, multi_agent_version: Option, @@ -2021,7 +2071,8 @@ async fn record_initial_history_seeds_token_info_from_rollout() { #[tokio::test] async fn recompute_token_usage_uses_session_base_instructions() { - let (session, turn_context) = make_session_and_context().await; + let (session, mut turn_context) = make_session_and_context().await; + turn_context.inline_instructions = false; let override_instructions = "SESSION_OVERRIDE_INSTRUCTIONS_ONLY".repeat(120); { @@ -2726,6 +2777,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() { let previous_model = "forked-rollout-model"; let previous_context_item = TurnContextItem { turn_id: Some(turn_context.sub_id.clone()), + inline_instructions: false, #[allow(deprecated)] cwd: turn_context.cwd.clone(), workspace_roots: None, diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index a35d9b60ca..3a2c8e4444 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -1071,7 +1071,7 @@ pub(crate) fn build_prompt( input, tools: router.model_visible_specs(), parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls, - base_instructions, + base_instructions: (!turn_context.inline_instructions).then_some(base_instructions), output_schema: turn_context.final_output_json_schema.clone(), output_schema_strict: !crate::guardian::is_guardian_reviewer_source( &turn_context.session_source, diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index bb434fa2c9..0d9414b8e4 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -104,6 +104,7 @@ pub struct TurnContext { pub(crate) sub_id: String, pub(crate) trace_id: Option, pub(crate) realtime_active: bool, + pub(crate) inline_instructions: bool, pub config: Arc, pub(crate) auth_manager: Option>, pub(crate) model_info: ModelInfo, @@ -257,6 +258,7 @@ impl TurnContext { sub_id: self.sub_id.clone(), trace_id: self.trace_id.clone(), realtime_active: self.realtime_active, + inline_instructions: self.inline_instructions, config: Arc::new(config), auth_manager: self.auth_manager.clone(), model_info: model_info.clone(), @@ -370,6 +372,7 @@ impl TurnContext { let cwd = self.cwd.clone(); TurnContextItem { turn_id: Some(self.sub_id.clone()), + inline_instructions: self.inline_instructions, cwd, workspace_roots: (!workspace_roots.is_empty()).then_some(workspace_roots), current_date: self.current_date.clone(), @@ -551,6 +554,7 @@ impl Session { sub_id, trace_id: current_span_trace_id(), realtime_active: false, + inline_instructions: true, config: per_turn_config, auth_manager: auth_manager_for_context, model_info, @@ -782,6 +786,10 @@ impl Session { sub_id, skills_snapshot, ); + turn_context.inline_instructions = self + .reference_context_item() + .await + .is_none_or(|item| item.inline_instructions); turn_context.realtime_active = self.conversation.running_state().await.is_some(); if let Some(final_schema) = final_output_json_schema { diff --git a/codex-rs/core/tests/common/context_snapshot.rs b/codex-rs/core/tests/common/context_snapshot.rs index 06e153da33..7eea5c7352 100644 --- a/codex-rs/core/tests/common/context_snapshot.rs +++ b/codex-rs/core/tests/common/context_snapshot.rs @@ -58,8 +58,34 @@ pub fn format_request_input_snapshot( request: &ResponsesRequest, options: &ContextSnapshotOptions, ) -> String { - let items = request.input(); - format_response_items_snapshot(items.as_slice(), options) + let mut items = request.input(); + let has_top_level_instructions = request + .body_json() + .get("instructions") + .and_then(Value::as_str) + .is_some(); + // Inline model instructions can be large and vary by model. Keep their position visible while + // normalizing the text to a stable, compact placeholder. + if !has_top_level_instructions + && let Some(text) = items + .iter_mut() + .find(|item| { + item.get("type").and_then(Value::as_str) == Some("message") + && item.get("role").and_then(Value::as_str) == Some("developer") + }) + .and_then(|item| item.get_mut("content")) + .and_then(Value::as_array_mut) + .and_then(|content| content.first_mut()) + .and_then(|entry| entry.get_mut("text")) + { + *text = Value::String("".to_string()); + } + let input = format_response_items_snapshot(items.as_slice(), options); + if has_top_level_instructions { + format!("instructions:\n{input}") + } else { + input + } } pub fn format_response_items_snapshot(items: &[Value], options: &ContextSnapshotOptions) -> String { @@ -422,7 +448,7 @@ fn canonicalize_snapshot_text(text: &str) -> String { { return format!("\n{summary}"); } - normalize_dynamic_snapshot_paths(text) + normalize_dynamic_snapshot_text(text) } fn is_capability_instruction_text(text: &str) -> bool { @@ -431,14 +457,21 @@ fn is_capability_instruction_text(text: &str) -> bool { || text.starts_with(PLUGINS_INSTRUCTIONS_OPEN_TAG) } -fn normalize_dynamic_snapshot_paths(text: &str) -> String { +fn normalize_dynamic_snapshot_text(text: &str) -> String { static SYSTEM_SKILL_PATH_RE: OnceLock = OnceLock::new(); + static WALL_TIME_RE: OnceLock = OnceLock::new(); let system_skill_path_re = SYSTEM_SKILL_PATH_RE.get_or_init(|| { Regex::new(r"/[^)\n]*/skills/\.system/([^/\n]+)/SKILL\.md") .expect("system skill path regex should compile") }); - system_skill_path_re + let text = system_skill_path_re .replace_all(text, "/$1/SKILL.md") + .into_owned(); + let wall_time_re = WALL_TIME_RE.get_or_init(|| { + Regex::new(r"Wall time: [0-9.]+ seconds").expect("wall time regex should compile") + }); + wall_time_re + .replace_all(&text, "Wall time: ") .into_owned() } diff --git a/codex-rs/core/tests/suite/auto_review.rs b/codex-rs/core/tests/suite/auto_review.rs index 4dd3effe49..b913a252ab 100644 --- a/codex-rs/core/tests/suite/auto_review.rs +++ b/codex-rs/core/tests/suite/auto_review.rs @@ -201,9 +201,9 @@ async fn remote_model_override_uses_catalog_model_for_strict_auto_review() -> Re .into_iter() .find(|request| { request.body_contains_text("auto-review-model-override.txt") - && request - .instructions_text() - .starts_with("You are judging one planned coding-agent action.") + && request.message_input_texts("developer").iter().any(|text| { + text.starts_with("You are judging one planned coding-agent action.") + }) }) .expect("expected Guardian request for apply_patch"); assert_eq!( diff --git a/codex-rs/core/tests/suite/cli_stream.rs b/codex-rs/core/tests/suite/cli_stream.rs index d2eac6fb3c..0df19c4685 100644 --- a/codex-rs/core/tests/suite/cli_stream.rs +++ b/codex-rs/core/tests/suite/cli_stream.rs @@ -344,14 +344,9 @@ async fn exec_cli_applies_model_instructions_file() { assert!(output.status.success()); // Inspect the captured request and verify our custom base instructions were - // included in the `instructions` field. + // included in developer input. let request = resp_mock.single_request(); - let body = request.body_json(); - let instructions = body - .get("instructions") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); + let instructions = request.message_input_texts("developer").join("\n\n"); assert!( instructions.contains(marker), "instructions did not contain custom marker; got: {instructions}" @@ -414,12 +409,7 @@ async fn exec_cli_profile_applies_model_instructions_file() { assert!(output.status.success()); let request = resp_mock.single_request(); - let body = request.body_json(); - let instructions = body - .get("instructions") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); + let instructions = request.message_input_texts("developer").join("\n\n"); assert!( instructions.contains(marker), "instructions did not contain profile marker; got: {instructions}" diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index fe543a875b..19a0a5ce09 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -1199,7 +1199,7 @@ async fn send_provider_auth_request(server: &MockServer, auth: ModelProviderAuth } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn includes_base_instructions_override_in_request() { +async fn inlines_base_instructions_override_in_request() { skip_if_no_network!(); // Mock server let server = MockServer::start().await; @@ -1238,12 +1238,14 @@ async fn includes_base_instructions_override_in_request() { let request = resp_mock.single_request(); let request_body = request.body_json(); + let developer_messages = request.message_input_texts("developer"); + assert!(request_body.get("instructions").is_none()); assert!( - request_body["instructions"] - .as_str() - .unwrap() - .contains("test instructions") + developer_messages + .iter() + .any(|text| text.contains("test instructions")), + "expected base instructions in developer input, got {developer_messages:?}" ); } @@ -1464,16 +1466,13 @@ async fn includes_user_instructions_message_in_request() { let request = resp_mock.single_request(); let request_body = request.body_json(); - assert!( - !request_body["instructions"] - .as_str() - .unwrap() - .contains("be nice") - ); + assert!(request_body.get("instructions").is_none()); assert_message_role(&request_body["input"][0], "developer"); - let permissions_text = request_body["input"][0]["content"][0]["text"] - .as_str() - .expect("invalid permissions message content"); + let developer_texts = request.message_input_texts("developer"); + let permissions_text = developer_texts + .iter() + .find(|text| text.contains("`sandbox_mode`")) + .expect("permissions message content"); assert!( permissions_text.contains("`sandbox_mode`"), "expected permissions message to mention sandbox_mode, got {permissions_text:?}" @@ -2650,17 +2649,13 @@ async fn includes_developer_instructions_message_in_request() { let request = resp_mock.single_request(); let request_body = request.body_json(); - let permissions_text = request_body["input"][0]["content"][0]["text"] - .as_str() - .expect("invalid permissions message content"); - - assert!( - !request_body["instructions"] - .as_str() - .unwrap() - .contains("be nice") - ); + assert!(request_body.get("instructions").is_none()); assert_message_role(&request_body["input"][0], "developer"); + let developer_texts = request.message_input_texts("developer"); + let permissions_text = developer_texts + .iter() + .find(|text| text.contains("`sandbox_mode`")) + .expect("permissions message content"); assert!( permissions_text.contains("`sandbox_mode`"), "expected permissions message to mention sandbox_mode, got {permissions_text:?}" diff --git a/codex-rs/core/tests/suite/client_websockets.rs b/codex-rs/core/tests/suite/client_websockets.rs index 1ccbd302d8..155d9bcff0 100755 --- a/codex-rs/core/tests/suite/client_websockets.rs +++ b/codex-rs/core/tests/suite/client_websockets.rs @@ -2081,9 +2081,9 @@ fn prompt_with_input(input: Vec) -> Prompt { fn prompt_with_input_and_instructions(input: Vec, instructions: &str) -> Prompt { let mut prompt = prompt_with_input(input); - prompt.base_instructions = BaseInstructions { + prompt.base_instructions = Some(BaseInstructions { text: instructions.to_string(), - }; + }); prompt } diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index 92ec66e161..1206831033 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -547,11 +547,13 @@ async fn summarize_context_three_requests_and_instructions() { let body3 = requests[2].body_json(); // Manual compact should keep the baseline developer instructions. - let instr1 = body1.get("instructions").and_then(|v| v.as_str()).unwrap(); - let instr2 = body2.get("instructions").and_then(|v| v.as_str()).unwrap(); - assert_eq!( - instr1, instr2, - "manual compact should keep the standard developer instructions" + assert!(body1.get("instructions").is_none()); + assert!(body2.get("instructions").is_none()); + let developer_texts1 = requests[0].message_input_texts("developer"); + let developer_texts2 = requests[1].message_input_texts("developer"); + assert!( + developer_texts2.starts_with(&developer_texts1), + "manual compact should keep the standard developer instructions: {developer_texts2:?}" ); // The summarization request should include the injected user input marker. @@ -1206,18 +1208,20 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { return None; } - let text = value - .get("content") - .and_then(|content| content.as_array()) - .and_then(|content| content.first()) - .and_then(|item| item.get("text")) - .and_then(|text| text.as_str()); - // Ignore cached prefix messages (project docs + permissions) since they are not // relevant to compaction behavior and can change as bundled prompts evolve. let role = value.get("role").and_then(|role| role.as_str()); if role == Some("developer") - && text.is_some_and(|text| text.contains("`sandbox_mode`")) + && value + .get("content") + .and_then(|content| content.as_array()) + .is_some_and(|content| { + content.iter().any(|item| { + item.get("text") + .and_then(|text| text.as_str()) + .is_some_and(|text| text.contains("`sandbox_mode`")) + }) + }) { return None; } @@ -1713,18 +1717,13 @@ async fn auto_compact_runs_after_token_limit_hit() { let body_first = requests[0].body_json(); let body_auto = requests[auto_compact_index].body_json(); let body_follow_up = requests[follow_up_index].body_json(); - let instructions = body_auto - .get("instructions") - .and_then(|v| v.as_str()) - .unwrap_or_default(); - let baseline_instructions = body_first - .get("instructions") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); - assert_eq!( - instructions, baseline_instructions, - "auto compact should keep the standard developer instructions", + assert!(body_first.get("instructions").is_none()); + assert!(body_auto.get("instructions").is_none()); + let baseline_developer_texts = requests[0].message_input_texts("developer"); + let auto_developer_texts = requests[auto_compact_index].message_input_texts("developer"); + assert!( + auto_developer_texts.starts_with(&baseline_developer_texts), + "auto compact should keep the standard developer instructions: {auto_developer_texts:?}", ); let input_auto = body_auto.get("input").and_then(|v| v.as_array()).unwrap(); diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 77561093c6..de98277ba1 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -61,8 +61,12 @@ fn estimate_compact_input_tokens(request: &responses::ResponsesRequest) -> i64 { } fn estimate_compact_payload_tokens(request: &responses::ResponsesRequest) -> i64 { - estimate_compact_input_tokens(request) - .saturating_add(approx_token_count(&request.instructions_text())) + let body = request.body_json(); + let instructions = body + .get("instructions") + .and_then(Value::as_str) + .unwrap_or_default(); + estimate_compact_input_tokens(request).saturating_add(approx_token_count(instructions)) } fn assert_tools_payload_does_not_defer(body: &Value) { @@ -1986,7 +1990,17 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result }), ) .await?; - let baseline_codex = baseline_harness.test().codex.clone(); + let baseline_test = baseline_harness.test(); + let baseline_base_instructions = baseline_test + .thread_manager + .get_models_manager() + .get_model_info( + &baseline_test.session_configured.model, + &baseline_test.config.to_models_manager_config(), + ) + .await + .base_instructions; + let baseline_codex = baseline_test.codex.clone(); responses::mount_sse_sequence( baseline_harness.server(), @@ -2071,12 +2085,13 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result let override_base_instructions = format!( "{}\nREMOTE_BASE_INSTRUCTIONS_OVERRIDE {}", - baseline_compact_request.instructions_text(), + baseline_base_instructions, "x".repeat(8_000) ); let override_context_window = baseline_payload_tokens.saturating_add(500); - let pretrim_override_estimate = - baseline_input_tokens.saturating_add(approx_token_count(&override_base_instructions)); + let pretrim_override_estimate = baseline_input_tokens + .saturating_sub(approx_token_count(&baseline_base_instructions)) + .saturating_add(approx_token_count(&override_base_instructions)); assert!( pretrim_override_estimate > override_context_window, "expected override instructions to push pre-trim estimate past the context window" @@ -2165,9 +2180,18 @@ async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result .await; let override_compact_request = override_compact_mock.single_request(); - assert_eq!( - override_compact_request.instructions_text(), - override_base_instructions + assert!( + override_compact_request + .body_json() + .get("instructions") + .is_none() + ); + let override_developer_texts = override_compact_request.message_input_texts("developer"); + assert!( + override_developer_texts + .iter() + .any(|text| text.contains(&override_base_instructions)), + "expected override instructions in developer input, got {override_developer_texts:?}" ); assert!( override_compact_request.has_function_call(override_retained_call_id), diff --git a/codex-rs/core/tests/suite/mcp_turn_metadata.rs b/codex-rs/core/tests/suite/mcp_turn_metadata.rs index 0d403180d7..9fc24f5f8a 100644 --- a/codex-rs/core/tests/suite/mcp_turn_metadata.rs +++ b/codex-rs/core/tests/suite/mcp_turn_metadata.rs @@ -317,8 +317,9 @@ async fn apps_default_prompt_with_auto_review_routes_actual_mcp_approval_to_guar .into_iter() .find(|request| { request - .instructions_text() - .starts_with("You are judging one planned coding-agent action.") + .message_input_texts("developer") + .iter() + .any(|text| text.starts_with("You are judging one planned coding-agent action.")) }) .expect("expected a Guardian request for the app MCP approval"); assert!(guardian_request.body_contains_text("calendar_create_event")); diff --git a/codex-rs/core/tests/suite/model_visible_layout.rs b/codex-rs/core/tests/suite/model_visible_layout.rs index ba20da039e..61905a8fe7 100644 --- a/codex-rs/core/tests/suite/model_visible_layout.rs +++ b/codex-rs/core/tests/suite/model_visible_layout.rs @@ -6,9 +6,12 @@ use anyhow::Result; use codex_config::types::Personality; use codex_features::Feature; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::Op; +use codex_protocol::protocol::RolloutItem; +use codex_protocol::protocol::RolloutLine; use codex_protocol::user_input::UserInput; use core_test_support::PathBufExt; use core_test_support::context_snapshot; @@ -374,6 +377,40 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let initial_request = initial_mock.single_request(); + // Rewrite the seed turn into the legacy representation: no persisted compatibility flag and + // no model instructions in history. The resumed request should therefore use the top-level + // `instructions` field. + let mut removed_inline_instructions = false; + let mut found_turn_context = false; + let legacy_rollout = fs::read_to_string(&rollout_path)? + .lines() + .map(|line| -> Result { + let mut line: RolloutLine = serde_json::from_str(line)?; + match &mut line.item { + RolloutItem::TurnContext(turn_context) => { + turn_context.inline_instructions = false; + found_turn_context = true; + } + RolloutItem::ResponseItem(ResponseItem::Message { role, content, .. }) + if role == "developer" && !removed_inline_instructions => + { + assert!(!content.is_empty(), "developer message should have content"); + content.remove(0); + removed_inline_instructions = true; + } + _ => {} + } + Ok(serde_json::to_string(&line)?) + }) + .collect::>>()? + .join("\n"); + assert!(found_turn_context, "rollout should contain a turn context"); + assert!( + removed_inline_instructions, + "rollout should contain inline model instructions" + ); + fs::write(&rollout_path, format!("{legacy_rollout}\n"))?; + let resumed_mock = mount_sse_once( &server, sse(vec![ @@ -437,7 +474,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul insta::assert_snapshot!( "model_visible_layout_resume_with_personality_change", format_labeled_requests_snapshot( - "First post-resume turn where resumed config model differs from rollout and personality changes.", + "First post-legacy-resume turn where resumed config model differs from rollout and personality changes.", &[ ("Last Request Before Resume", &initial_request), ("First Request After Resume", &resumed_request), diff --git a/codex-rs/core/tests/suite/personality.rs b/codex-rs/core/tests/suite/personality.rs index a58e5f20fe..c34de28abe 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -194,7 +194,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request = resp_mock.single_request(); - let instructions_text = request.instructions_text(); + let instructions_text = request.message_input_texts("developer").join("\n\n"); assert!( instructions_text.contains(LOCAL_FRIENDLY_TEMPLATE), @@ -241,7 +241,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request = resp_mock.single_request(); - let instructions_text = request.instructions_text(); + let instructions_text = request.message_input_texts("developer").join("\n\n"); assert!( !instructions_text.contains(LOCAL_FRIENDLY_TEMPLATE), "expected no friendly personality template, got: {instructions_text:?}" @@ -294,7 +294,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request = resp_mock.single_request(); - let instructions_text = request.instructions_text(); + let instructions_text = request.message_input_texts("developer").join("\n\n"); assert!( instructions_text.contains(LOCAL_PRAGMATIC_TEMPLATE), "expected default friendly template, got: {instructions_text:?}" @@ -636,7 +636,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; let request = resp_mock.single_request(); - let instructions_text = request.instructions_text(); + let instructions_text = request.message_input_texts("developer").join("\n\n"); assert!( instructions_text.contains(friendly_personality_message), diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index a455512321..1df9230ab0 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -110,7 +110,6 @@ fn normalize_newlines(text: &str) -> String { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { skip_if_no_network!(Ok(())); - use pretty_assertions::assert_eq; let server = start_mock_server().await; let req1 = mount_sse_once( @@ -198,7 +197,8 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { "tool_search", "web_search", ]); - let body0 = req1.single_request().body_json(); + let request0 = req1.single_request(); + let body0 = request0.body_json(); let expected_instructions = if expected_tools_names.contains(&"apply_patch") { base_instructions @@ -206,16 +206,25 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { [base_instructions, APPLY_PATCH_TOOL_INSTRUCTIONS.to_string()].join("\n") }; - assert_eq!( - body0["instructions"], - serde_json::json!(expected_instructions), + assert!(body0.get("instructions").is_none()); + let developer_texts0 = request0.message_input_texts("developer"); + assert!( + developer_texts0 + .iter() + .any(|text| text.starts_with(&expected_instructions)), + "expected model instructions in developer input, got {developer_texts0:?}" ); assert_tool_names(&body0, &expected_tools_names); - let body1 = req2.single_request().body_json(); - assert_eq!( - body1["instructions"], - serde_json::json!(expected_instructions), + let request1 = req2.single_request(); + let body1 = request1.body_json(); + assert!(body1.get("instructions").is_none()); + let developer_texts1 = request1.message_input_texts("developer"); + assert!( + developer_texts1 + .iter() + .any(|text| text.starts_with(&expected_instructions)), + "expected model instructions in developer input, got {developer_texts1:?}" ); assert_tool_names(&body1, &expected_tools_names); @@ -280,19 +289,19 @@ async fn gpt_5_tools_without_apply_patch_append_apply_patch_instructions() -> an wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - let body0 = req1.single_request().body_json(); - let instructions0 = body0["instructions"] - .as_str() - .expect("instructions should be a string"); + let request0 = req1.single_request(); + assert!(request0.body_json().get("instructions").is_none()); + let developer_texts0 = request0.message_input_texts("developer"); + let instructions0 = developer_texts0.first().expect("developer instructions"); assert!( instructions0.contains("You are"), "expected non-empty instructions" ); - let body1 = req2.single_request().body_json(); - let instructions1 = body1["instructions"] - .as_str() - .expect("instructions should be a string"); + let request1 = req2.single_request(); + assert!(request1.body_json().get("instructions").is_none()); + let developer_texts1 = request1.message_input_texts("developer"); + let instructions1 = developer_texts1.first().expect("developer instructions"); assert_eq!( normalize_newlines(instructions1), normalize_newlines(instructions0) @@ -647,15 +656,13 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul let permissions_texts: Vec<&str> = input .iter() - .filter_map(|msg| { - let role = msg["role"].as_str()?; - if role != "developer" { - return None; - } + .filter(|msg| msg["role"].as_str() == Some("developer")) + .flat_map(|msg| { msg["content"] .as_array() - .and_then(|content| content.first()) - .and_then(|item| item["text"].as_str()) + .into_iter() + .flatten() + .filter_map(|item| item["text"].as_str()) }) .collect(); assert!( diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index b63aff5400..c1d364284a 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -826,9 +826,15 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { let base_model_info = models_manager .get_model_info("gpt-5.2", &config.to_models_manager_config()) .await; - let body = response_mock.single_request().body_json(); - let instructions = body["instructions"].as_str().unwrap(); - assert_eq!(instructions, base_model_info.base_instructions); + let request = response_mock.single_request(); + assert!(request.body_json().get("instructions").is_none()); + let developer_texts = request.message_input_texts("developer"); + assert!( + developer_texts + .iter() + .any(|text| text.contains(&base_model_info.base_instructions)), + "expected model instructions in developer input, got {developer_texts:?}" + ); Ok(()) } diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 8f760db417..10c954c8de 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -274,12 +274,9 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; - let initial_body = initial_mock.single_request().body_json(); - let initial_instructions = initial_body - .get("instructions") - .and_then(|v| v.as_str()) - .unwrap_or_default() - .to_string(); + let initial_request = initial_mock.single_request(); + assert!(initial_request.body_json().get("instructions").is_none()); + let initial_developer_texts = initial_request.message_input_texts("developer"); let resumed_mock = mount_sse_sequence( &server, @@ -342,8 +339,9 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { assert_eq!(requests.len(), 2, "expected two resumed requests"); let first_resumed = &requests[0]; - assert_eq!(first_resumed.instructions_text(), initial_instructions); + assert!(first_resumed.body_json().get("instructions").is_none()); let first_developer_texts = first_resumed.message_input_texts("developer"); + assert!(first_developer_texts.starts_with(&initial_developer_texts)); let first_model_switch_count = first_developer_texts .iter() .filter(|text| text.contains("")) @@ -354,8 +352,9 @@ async fn resume_switches_models_preserves_base_instructions() -> Result<()> { ); let second_resumed = &requests[1]; - assert_eq!(second_resumed.instructions_text(), initial_instructions); + assert!(second_resumed.body_json().get("instructions").is_none()); let second_developer_texts = second_resumed.message_input_texts("developer"); + assert!(second_developer_texts.starts_with(&initial_developer_texts)); let second_model_switch_count = second_developer_texts .iter() .filter(|text| text.contains("")) diff --git a/codex-rs/core/tests/suite/resume_warning.rs b/codex-rs/core/tests/suite/resume_warning.rs index 073e6d8b49..ede936d02d 100644 --- a/codex-rs/core/tests/suite/resume_warning.rs +++ b/codex-rs/core/tests/suite/resume_warning.rs @@ -27,6 +27,7 @@ fn resume_history( let turn_id = "resume-warning-seed-turn".to_string(); let turn_ctx = TurnContextItem { turn_id: Some(turn_id.clone()), + inline_instructions: false, cwd: config.cwd.clone(), workspace_roots: None, current_date: None, diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index 6b0f8552a2..73dcb0dbb8 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -627,9 +627,15 @@ async fn review_input_isolated_from_parent_history() { "user message should only contain the raw review prompt" ); - // Ensure the REVIEW_PROMPT rubric is sent via instructions. - let instructions = body["instructions"].as_str().expect("instructions string"); - assert_eq!(instructions, REVIEW_PROMPT); + // Ensure the REVIEW_PROMPT rubric is sent via developer input. + assert!(body.get("instructions").is_none()); + let developer_texts = request.message_input_texts("developer"); + assert!( + developer_texts + .iter() + .any(|text| text.contains(REVIEW_PROMPT)), + "expected review instructions in developer input, got {developer_texts:?}" + ); // Also verify that a user interruption note was recorded in the rollout. let path = codex.rollout_path().expect("rollout path"); diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap b/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap index fefdfde60c..1d3a1ff052 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__additional_context__additional_context_simple_input.snap @@ -1,11 +1,13 @@ --- source: core/tests/suite/additional_context.rs -expression: "context_snapshot::format_labeled_requests_snapshot(\"additional context is inserted before the user turn input.\",\n&[(\"Request\", &request)], &additional_context_snapshot_options(),)" +expression: "context_snapshot::format_labeled_requests_snapshot(\"additional context is inserted before the user turn input.\",\n&[(\"Request\", &request)],\n&ContextSnapshotOptions::default().strip_capability_instructions().render_mode(ContextSnapshotRenderMode::KindWithTextPrefix\n{ max_chars: 160 }),)" --- Scenario: additional context is inserted before the user turn input. ## Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/developer:run one 02:message/user:tab one 03:message/user:inspect the active tab diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap index daa7700601..a0050c992d 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_with_history_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"Manual /compact with prior user Scenario: Manual /compact with prior user history compacts existing history and the follow-up turn includes the compact summary plus new user message. ## Local Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:first manual turn 03:message/assistant:FIRST_REPLY @@ -14,6 +16,8 @@ Scenario: Manual /compact with prior user history compacts existing history and ## Local Post-Compaction History Layout 00:message/user:first manual turn 01:message/user:\nFIRST_MANUAL_SUMMARY -02:message/developer: +02:message/developer[2]: + [01] + [02] 03:message/user:> 04:message/user:second manual turn diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap index 6007a02a11..6a60dbcde7 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__manual_compact_without_prev_user_shapes.snap @@ -9,6 +9,8 @@ Scenario: Manual /compact with no prior user turn currently still issues a compa ## Local Post-Compaction History Layout 00:message/user:\nMANUAL_EMPTY_SUMMARY -01:message/developer: +01:message/developer[2]: + [01] + [02] 02:message/user:> 03:message/user:AFTER_MANUAL_EMPTY_COMPACT diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap index ab46355e35..8544730d4a 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__mid_turn_compaction_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"True mid-turn continuation compa Scenario: True mid-turn continuation compaction after tool output: compact request includes tool artifacts, and the continuation request includes the summary in the same turn. ## Local Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:function call limit push 03:function_call/test_tool @@ -13,7 +15,9 @@ Scenario: True mid-turn continuation compaction after tool output: compact reque 05:message/user: ## Local Post-Compaction History Layout -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:function call limit push 03:message/user:\nAUTO_SUMMARY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap index d63924a44a..ccd8aa9939 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_sampling_model_switch_compaction_shapes.snap @@ -5,12 +5,16 @@ expression: "format_labeled_requests_snapshot(\"Pre-sampling compaction on model Scenario: Pre-sampling compaction on model switch to a smaller context window: current behavior compacts using prior-turn history only (incoming user message excluded), and the follow-up request carries compacted history plus the new user message. ## Initial Request (Previous Model) -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:before switch ## Pre-sampling Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:before switch 03:message/assistant:before switch @@ -19,8 +23,9 @@ Scenario: Pre-sampling compaction on model switch to a smaller context window: c ## Post-Compaction Follow-up Request (Next Model) 00:message/user:before switch 01:message/user:\nPRE_SAMPLING_SUMMARY -02:message/developer[2]: - [01] \nThe user was previously using a different model.... - [02] +02:message/developer[3]: + [01] + [02] \nThe user was previously using a different model.... + [03] 03:message/user:> 04:message/user:after switch diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap index 9df96774c8..5ee2e58246 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_context_window_exceeded_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction context Scenario: Pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors. ## Local Compaction Request (Incoming User Excluded) -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:FIRST_REPLY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap index 679081353e..6b5ea3560f 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_including_incoming_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"Pre-turn auto-compaction with a Scenario: Pre-turn auto-compaction with a context override emits the context diff in the compact request while the incoming user message is still excluded. ## Local Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:FIRST_REPLY @@ -17,7 +19,9 @@ Scenario: Pre-turn auto-compaction with a context override emits the context dif 00:message/user:USER_ONE 01:message/user:USER_TWO 02:message/user:\nPRE_TURN_SUMMARY -03:message/developer: +03:message/developer[2]: + [01] + [02] 04:message/user: 05:message/user[2]: [01] diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap index f00c13919b..d4abc113cb 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact__pre_turn_compaction_strips_incoming_model_switch_shapes.snap @@ -5,12 +5,16 @@ expression: "format_labeled_requests_snapshot(\"Pre-turn compaction during model Scenario: Pre-turn compaction during model switch (without pre-sampling model-switch compaction): current behavior strips incoming from the compact request and restores it in the post-compaction follow-up request. ## Initial Request (Previous Model) -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:BEFORE_SWITCH_USER ## Local Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:BEFORE_SWITCH_USER 03:message/assistant:BEFORE_SWITCH_REPLY @@ -19,9 +23,10 @@ Scenario: Pre-turn compaction during model switch (without pre-sampling model-sw ## Local Post-Compaction History Layout 00:message/user:BEFORE_SWITCH_USER 01:message/user:\nPRETURN_SWITCH_SUMMARY -02:message/developer[3]: - [01] \nThe user was previously using a different model.... - [02] - [03] The user has requested a new communication st... +02:message/developer[4]: + [01] + [02] \nThe user was previously using a different model.... + [03] + [04] The user has requested a new communication st... 03:message/user:> 04:message/user:AFTER_SWITCH_USER diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_compact_resume_restates_realtime_end_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_compact_resume_restates_realtime_end_shapes.snap index 0289370633..4154c61f30 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_compact_resume_restates_realtime_end_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_compact_resume_restates_realtime_end_shapes.snap @@ -5,17 +5,19 @@ expression: "format_labeled_requests_snapshot(\"After remote manual /compact and Scenario: After remote manual /compact and resume, the first resumed turn rebuilds history from the compaction item and restates realtime-end instructions from reconstructed previous-turn settings. ## Remote Compaction Request -00:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +00:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:REMOTE_FIRST_REPLY ## Remote Post-Resume History Layout 00:compaction:encrypted=true -01:message/developer[2]: - [01] - [02] \nRealtime conversation ended.\n\nSubsequ... +01:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation ended.\n\nSubsequ... 02:message/user:> 03:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_restates_realtime_start_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_restates_realtime_start_shapes.snap index 400e6d502b..787f2f9810 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_restates_realtime_start_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_restates_realtime_start_shapes.snap @@ -5,17 +5,19 @@ expression: "format_labeled_requests_snapshot(\"Remote manual /compact while rea Scenario: Remote manual /compact while realtime remains active: the next regular turn restates realtime-start instructions after compaction clears the baseline. ## Remote Compaction Request -00:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +00:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:REMOTE_FIRST_REPLY ## Remote Post-Compaction History Layout 00:compaction:encrypted=true -01:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +01:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 02:message/user:> 03:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap index 8b61ee6158..aa3f19e8ff 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_with_history_shapes.snap @@ -5,13 +5,17 @@ expression: "format_labeled_requests_snapshot(\"Remote manual /compact where rem Scenario: Remote manual /compact where remote compact output is compaction-only: follow-up layout uses the returned compaction item plus new user message. ## Remote Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:hello remote compact 03:message/assistant:FIRST_REMOTE_REPLY ## Remote Post-Compaction History Layout 00:compaction:encrypted=true -01:message/developer: +01:message/developer[2]: + [01] + [02] 02:message/user:> 03:message/user:after compact diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap index 5a616330b8..84107d2686 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_manual_compact_without_prev_user_shapes.snap @@ -5,6 +5,8 @@ expression: "format_labeled_requests_snapshot(\"Remote manual /compact with no p Scenario: Remote manual /compact with no prior user turn skips the remote compact request; the follow-up turn carries canonical context and new user message. ## Remote Post-Compaction History Layout -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_does_not_restate_realtime_end_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_does_not_restate_realtime_end_shapes.snap index 1e5021a58c..22eeb25f92 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_does_not_restate_realtime_end_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_does_not_restate_realtime_end_shapes.snap @@ -5,9 +5,10 @@ expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation com Scenario: Remote mid-turn continuation compaction after realtime was closed before the turn: the initial second-turn request emits realtime-end instructions, but the continuation request does not restate them after compaction because the current turn already established the inactive baseline. ## Second Turn Initial Request -00:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +00:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 01:message/user:> 02:message/user:SETUP_USER 03:message/assistant:REMOTE_SETUP_REPLY @@ -15,9 +16,10 @@ Scenario: Remote mid-turn continuation compaction after realtime was closed befo 05:message/user:USER_TWO ## Remote Compaction Request -00:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +00:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 01:message/user:> 02:message/user:SETUP_USER 03:message/assistant:REMOTE_SETUP_REPLY @@ -27,6 +29,8 @@ Scenario: Remote mid-turn continuation compaction after realtime was closed befo 07:function_call_output:unsupported call: test_tool ## Remote Post-Compaction History Layout -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:compaction:encrypted=true diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap index e84d4352de..a5594bf2c5 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes.snap @@ -7,10 +7,19 @@ Scenario: After a prior manual /compact produced an older remote compaction item ## Remote Compaction Request 00:message/user:USER_ONE 01:compaction:encrypted=true +02:message/developer[2]: + [01] + [02] +03:message/user:> +04:message/user:USER_TWO +05:function_call/shell_command +06:function_call_output:Exit code: 0\nWall time: \nOutput:\nmulti-summary\n ## Second Turn Request (After Compaction) 00:message/user:USER_ONE 01:compaction:encrypted=true -02:message/developer: +02:message/developer[2]: + [01] + [02] 03:message/user:> 04:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap index 388aee9981..b403dcd472 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_shapes.snap @@ -5,14 +5,18 @@ expression: "format_labeled_requests_snapshot(\"Remote mid-turn continuation com Scenario: Remote mid-turn continuation compaction after tool output: compact request includes tool artifacts and the follow-up request includes the returned compaction item. ## Remote Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:function_call/test_tool 04:function_call_output:unsupported call: test_tool ## Remote Post-Compaction History Layout -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:compaction:encrypted=true diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap index 5633154dc6..db85d39bea 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_mid_turn_compaction_summary_only_reinjects_context_shapes.snap @@ -5,13 +5,17 @@ expression: "format_labeled_requests_snapshot(\"Remote mid-turn compaction where Scenario: Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item. ## Remote Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:function_call/test_tool 04:function_call_output:unsupported call: test_tool ## Remote Post-Compaction History Layout -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:compaction:encrypted=true diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap index 4c76442816..416b0cf840 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_context_window_exceeded_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction Scenario: Remote pre-turn auto-compaction context-window failure: compaction request excludes the incoming user message and the turn errors. ## Remote Compaction Request (Incoming User Excluded) -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:REMOTE_FIRST_REPLY diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap index b6644e749c..6b4719e4b0 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_failure_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction Scenario: Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops. ## Remote Compaction Request (Incoming User Excluded) -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:turn that exceeds token threshold 03:message/assistant:initial turn complete diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap index d1192b4da1..de0f6b91da 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_including_incoming_shapes.snap @@ -5,7 +5,9 @@ expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction Scenario: Remote pre-turn auto-compaction with a context override emits the context diff in the compact request while excluding the incoming user message. ## Remote Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:REMOTE_FIRST_REPLY @@ -16,6 +18,8 @@ Scenario: Remote pre-turn auto-compaction with a context override emits the cont 00:message/user:USER_ONE 01:message/user:USER_TWO 02:compaction:encrypted=true -03:message/developer: +03:message/developer[2]: + [01] + [02] 04:message/user: 05:message/user:USER_THREE diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_end_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_end_shapes.snap index c00b9dcce8..36017deb2b 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_end_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_end_shapes.snap @@ -5,17 +5,19 @@ expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction Scenario: Remote pre-turn auto-compaction after realtime was closed between turns: the follow-up request emits realtime-end instructions from previous-turn settings even though compaction cleared the reference baseline. ## Remote Compaction Request -00:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +00:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:REMOTE_FIRST_REPLY ## Remote Post-Compaction History Layout 00:compaction:encrypted=true -01:message/developer[2]: - [01] - [02] \nRealtime conversation ended.\n\nSubsequ... +01:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation ended.\n\nSubsequ... 02:message/user:> 03:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_start_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_start_shapes.snap index 6de8837f1d..f078321946 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_start_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_restates_realtime_start_shapes.snap @@ -5,17 +5,19 @@ expression: "format_labeled_requests_snapshot(\"Remote pre-turn auto-compaction Scenario: Remote pre-turn auto-compaction while realtime remains active: compaction clears the reference baseline, so the follow-up request restates realtime-start instructions. ## Remote Compaction Request -00:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +00:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 01:message/user:> 02:message/user:USER_ONE 03:message/assistant:REMOTE_FIRST_REPLY ## Remote Post-Compaction History Layout 00:compaction:encrypted=true -01:message/developer[2]: - [01] - [02] \nRealtime conversation started.\n\nYou a... +01:message/developer[3]: + [01] + [02] + [03] \nRealtime conversation started.\n\nYou a... 02:message/user:> 03:message/user:USER_TWO diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap index 59aebbb234..452c9dc153 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_remote__remote_pre_turn_compaction_strips_incoming_model_switch_shapes.snap @@ -5,12 +5,16 @@ expression: "format_labeled_requests_snapshot(\"Remote pre-turn compaction durin Scenario: Remote pre-turn compaction during model switch currently excludes incoming user input, strips incoming from the compact request payload, and restores it in the post-compaction follow-up request. ## Initial Request (Previous Model) -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:BEFORE_SWITCH_USER ## Remote Compaction Request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:BEFORE_SWITCH_USER 03:message/assistant:BEFORE_SWITCH_REPLY @@ -18,9 +22,10 @@ Scenario: Remote pre-turn compaction during model switch currently excludes inco ## Remote Post-Compaction History Layout 00:message/user:BEFORE_SWITCH_USER 01:compaction:encrypted=true -02:message/developer[3]: - [01] \nThe user was previously using a different model.... - [02] - [03] The user has requested a new communication st... +02:message/developer[4]: + [01] + [02] \nThe user was previously using a different model.... + [03] + [04] The user has requested a new communication st... 03:message/user:> 04:message/user:AFTER_SWITCH_USER diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap index 925a810a43..594745362f 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_followup_turn_trims_context_updates.snap @@ -5,7 +5,9 @@ expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback trims Scenario: rollback trims pre-turn override context updates before the follow-up request ## rolled-back turn request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:turn 1 user 03:message/assistant:turn 1 assistant @@ -14,7 +16,9 @@ Scenario: rollback trims pre-turn override context updates before the follow-up 06:message/user:turn 2 user ## follow-up request after rollback -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:turn 1 user 03:message/assistant:turn 1 assistant diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap b/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap index bb694af693..cce58d2809 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__compact_resume_fork__rollback_past_compaction_shapes.snap @@ -5,7 +5,9 @@ expression: "context_snapshot::format_labeled_requests_snapshot(\"rollback past Scenario: rollback past compaction replay after rollback ## compaction request -00:message/developer: +00:message/developer[2]: + [01] + [02] 01:message/user:> 02:message/user:hello world 03:message/assistant:FIRST_REPLY @@ -14,13 +16,17 @@ Scenario: rollback past compaction replay after rollback ## before rollback 00:message/user:hello world 01:message/user:\nSUMMARY_ONLY_CONTEXT -02:message/developer: +02:message/developer[2]: + [01] + [02] 03:message/user:> 04:message/user:EDITED_AFTER_COMPACT ## after rollback 00:message/user:hello world 01:message/user:\nSUMMARY_ONLY_CONTEXT -02:message/developer: +02:message/developer[2]: + [01] + [02] 03:message/user:> 04:message/user:AFTER_ROLLBACK diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap index 9efdd98f77..ef3b3bbfc4 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_cwd_change_does_not_refresh_agents.snap @@ -5,16 +5,18 @@ expression: "format_labeled_requests_snapshot(\"Second turn changes cwd to a dir Scenario: Second turn changes cwd to a directory with different AGENTS.md; current behavior does not emit refreshed AGENTS instructions. ## First Request (agents_one) -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:first turn in agents_one ## Second Request (agents_two cwd) -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:first turn in agents_one 03:message/assistant:turn one complete diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap index 93f1c504b1..7a58e7c99c 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_override_matches_rollout_model.snap @@ -5,16 +5,18 @@ expression: "format_labeled_requests_snapshot(\"First post-resume turn where pre Scenario: First post-resume turn where pre-turn override sets model to rollout model; no model-switch update should appear. ## Last Request Before Resume -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:seed resume history ## First Request After Resume + Override -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:seed resume history 03:message/assistant:recorded before resume diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap index 42d1cd1a9f..fb1d4c81b2 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_resume_with_personality_change.snap @@ -1,17 +1,19 @@ --- source: core/tests/suite/model_visible_layout.rs -expression: "format_labeled_requests_snapshot(\"First post-resume turn where resumed config model differs from rollout and personality changes.\",\n&[(\"Last Request Before Resume\", &initial_request),\n(\"First Request After Resume\", &resumed_request),])" +expression: "format_labeled_requests_snapshot(\"First post-legacy-resume turn where resumed config model differs from rollout and personality changes.\",\n&[(\"Last Request Before Resume\", &initial_request),\n(\"First Request After Resume\", &resumed_request),])" --- -Scenario: First post-resume turn where resumed config model differs from rollout and personality changes. +Scenario: First post-legacy-resume turn where resumed config model differs from rollout and personality changes. ## Last Request Before Resume -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:seed resume history ## First Request After Resume +instructions: 00:message/developer[2]: [01] [02] diff --git a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap index 8e66e3314c..01ac4d9c97 100644 --- a/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap +++ b/codex-rs/core/tests/suite/snapshots/all__suite__model_visible_layout__model_visible_layout_turn_overrides.snap @@ -5,16 +5,18 @@ expression: "format_labeled_requests_snapshot(\"Second turn changes cwd, approva Scenario: Second turn changes cwd, approval policy, and personality while keeping model constant. ## First Request (Baseline) -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:first turn ## Second Request (Turn Overrides) -00:message/developer[2]: - [01] - [02] +00:message/developer[3]: + [01] + [02] + [03] 01:message/user:> 02:message/user:first turn 03:message/assistant:turn one complete diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 081649b7a4..0627df5307 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -3035,6 +3035,10 @@ pub struct TurnContextNetworkItem { pub struct TurnContextItem { #[serde(default, skip_serializing_if = "Option::is_none")] pub turn_id: Option, + /// Whether base model instructions are present in the model input as a developer message. + /// Missing values come from legacy rollouts that used the top-level Responses API field. + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub inline_instructions: bool, pub cwd: AbsolutePathBuf, /// Effective workspace roots used to materialize symbolic /// `:workspace_roots` filesystem permissions in `permission_profile`. @@ -5372,6 +5376,7 @@ mod tests { assert_eq!(item.network, None); assert_eq!(item.file_system_sandbox_policy, None); assert_eq!(item.comp_hash, None); + assert!(!item.inline_instructions); Ok(()) } @@ -5437,6 +5442,7 @@ mod tests { fn turn_context_item_serializes_network_when_present() -> Result<()> { let item = TurnContextItem { turn_id: None, + inline_instructions: true, cwd: test_path_buf("/tmp").abs(), workspace_roots: None, current_date: None, @@ -5468,6 +5474,7 @@ mod tests { }; let value = serde_json::to_value(item)?; + assert_eq!(value["inline_instructions"], true); assert_eq!( value["network"], json!({ diff --git a/codex-rs/rollout/src/recorder_tests.rs b/codex-rs/rollout/src/recorder_tests.rs index 8701578ebf..41e22017d6 100644 --- a/codex-rs/rollout/src/recorder_tests.rs +++ b/codex-rs/rollout/src/recorder_tests.rs @@ -1137,6 +1137,7 @@ async fn resume_candidate_matches_cwd_reads_latest_turn_context() -> std::io::Re timestamp: "2025-01-03T13:00:01Z".to_string(), item: RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), + inline_instructions: false, cwd: serde_json::from_value(serde_json::json!(&latest_cwd)) .expect("absolute latest cwd"), workspace_roots: None, diff --git a/codex-rs/state/src/extract.rs b/codex-rs/state/src/extract.rs index e156cc9113..6ba4fd89cb 100644 --- a/codex-rs/state/src/extract.rs +++ b/codex-rs/state/src/extract.rs @@ -349,6 +349,7 @@ mod tests { &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), + inline_instructions: false, cwd: serde_json::from_value(serde_json::json!( std::env::current_dir() .expect("current directory") @@ -394,6 +395,7 @@ mod tests { &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), + inline_instructions: false, cwd: serde_json::from_value(serde_json::json!( std::env::current_dir() .expect("current directory") @@ -439,6 +441,7 @@ mod tests { &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), + inline_instructions: false, cwd: serde_json::from_value(serde_json::json!(&fallback_cwd)) .expect("absolute fallback cwd"), workspace_roots: None, @@ -473,6 +476,7 @@ mod tests { &mut metadata, &RolloutItem::TurnContext(TurnContextItem { turn_id: Some("turn-1".to_string()), + inline_instructions: false, cwd: serde_json::from_value(serde_json::json!( std::env::current_dir() .expect("current directory")