use super::compact::allow_echo_commands; use core_test_support::test_codex::local_selections; use std::fs; use std::path::Path; use anyhow::Context; use anyhow::Result; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_config::test_support::CloudConfigBundleFixture; use codex_core::StartThreadOptions; use codex_core::TurnInputRequest; use codex_core::X_CODEX_ROUTING_HINT_HEADER; use codex_core::compact::SUMMARY_PREFIX; use codex_features::Feature; use codex_history::CodexHarnessMetadata; use codex_history::InitialHistory; use codex_history::RolloutItem; use codex_login::CodexAuth; use codex_login::auth::AgentIdentityAuth; use codex_login::auth::AgentIdentityAuthRecord; use codex_login::auth::BedrockApiKeyAuth; use codex_model_provider_info::AMAZON_BEDROCK_GPT_5_5_MODEL_ID; use codex_model_provider_info::AMAZON_BEDROCK_PROVIDER_ID; use codex_model_provider_info::ModelProviderInfo; use codex_protocol::AgentPath; use codex_protocol::account::PlanType as AccountPlanType; use codex_protocol::config_types::ServiceTier; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolFunctionSpec; use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec; use codex_protocol::dynamic_tools::DynamicToolNamespaceTool; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::models::ContentItem; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::openai_models::InputModality; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::ConversationStartParams; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::InterAgentCommunication; use codex_protocol::protocol::ItemCompletedEvent; use codex_protocol::protocol::ItemStartedEvent; use codex_protocol::protocol::Op; use codex_protocol::protocol::RealtimeConversationRealtimeEvent; use codex_protocol::protocol::RealtimeEvent; use codex_protocol::protocol::RealtimeOutputModality; use codex_protocol::protocol::ThreadHistoryMode; use codex_protocol::protocol::ThreadSettingsOverrides; use codex_protocol::user_input::UserInput; use core_test_support::PathBufExt; use core_test_support::apps_test_server::configure_search_capable_model; use core_test_support::context_snapshot; use core_test_support::context_snapshot::ContextSnapshotOptions; use core_test_support::context_snapshot::ContextSnapshotRenderMode; use core_test_support::responses; use core_test_support::responses::mount_sse_once; use core_test_support::responses::sse; use core_test_support::responses::start_websocket_server; use core_test_support::skip_if_no_network; use core_test_support::test_codex::TestCodexBuilder; use core_test_support::test_codex::TestCodexHarness; use core_test_support::test_codex::test_codex as base_test_codex; use core_test_support::test_path_buf; use core_test_support::wait_for_event; use core_test_support::wait_for_event_match; use core_test_support::wait_for_event_with_timeout; use pretty_assertions::assert_eq; use serde_json::Value; use serde_json::json; use test_case::test_case; use tokio::time::Duration; use wiremock::ResponseTemplate; const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str = "Output exceeded the available model context and was truncated"; const TEST_WAV_SAMPLE_RATE: u32 = 8_000; fn pcm_wav_data_url(sample_count: u32) -> String { let padding = sample_count % 2; let mut bytes = Vec::new(); bytes.extend_from_slice(b"RIFF"); bytes.extend_from_slice(&(36 + sample_count + padding).to_le_bytes()); bytes.extend_from_slice(b"WAVEfmt "); bytes.extend_from_slice(&16u32.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&TEST_WAV_SAMPLE_RATE.to_le_bytes()); bytes.extend_from_slice(&TEST_WAV_SAMPLE_RATE.to_le_bytes()); bytes.extend_from_slice(&1u16.to_le_bytes()); bytes.extend_from_slice(&8u16.to_le_bytes()); bytes.extend_from_slice(b"data"); bytes.extend_from_slice(&sample_count.to_le_bytes()); bytes.resize( bytes.len() + sample_count as usize + padding as usize, /*value*/ 0, ); format!("data:audio/wav;base64,{}", BASE64_STANDARD.encode(bytes)) } fn approx_token_count(text: &str) -> i64 { i64::try_from(text.len().saturating_add(3) / 4).unwrap_or(i64::MAX) } fn estimate_compact_input_tokens(request: &responses::ResponsesRequest) -> i64 { request.input().into_iter().fold(0i64, |acc, item| { acc.saturating_add(approx_token_count(&item.to_string())) }) } fn estimate_compact_payload_tokens(request: &responses::ResponsesRequest) -> i64 { estimate_compact_input_tokens(request) .saturating_add(approx_token_count(&request.instructions_text())) } fn assert_tools_payload_does_not_defer(body: &Value) { if let Some(tools) = body.get("tools") { assert!( !contains_defer_loading(tools), "model-visible tools should not include deferred declarations: {tools}" ); } } fn namespace_child_tool_names(body: &Value, namespace: &str) -> Vec { body.get("tools") .and_then(Value::as_array) .and_then(|tools| { tools.iter().find_map(|tool| { if tool.get("type").and_then(Value::as_str) == Some("namespace") && tool.get("name").and_then(Value::as_str) == Some(namespace) { tool.get("tools").and_then(Value::as_array).map(|children| { children .iter() .filter_map(|child| { child .get("name") .and_then(Value::as_str) .map(str::to_string) }) .collect() }) } else { None } }) }) .unwrap_or_default() } fn contains_defer_loading(value: &Value) -> bool { match value { Value::Object(map) => { map.get("defer_loading").and_then(Value::as_bool) == Some(true) || map.values().any(contains_defer_loading) } Value::Array(values) => values.iter().any(contains_defer_loading), Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => false, } } fn canonical_json(value: &Value) -> Value { match value { Value::Object(map) => { let mut entries = map.iter().collect::>(); entries.sort_by_key(|(left_key, _)| *left_key); Value::Object( entries .into_iter() .map(|(key, value)| (key.clone(), canonical_json(value))) .collect(), ) } Value::Array(values) => Value::Array(values.iter().map(canonical_json).collect()), Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => value.clone(), } } const PRETURN_CONTEXT_DIFF_CWD: &str = "/tmp/PRETURN_CONTEXT_DIFF_CWD"; const DUMMY_FUNCTION_NAME: &str = "test_tool"; const TURN_STATE_HEADER: &str = "x-codex-turn-state"; const REMOTE_COMPACT_TURN_COMPLETE_TIMEOUT: Duration = Duration::from_secs(30); const TEST_AGENT_IDENTITY_PRIVATE_KEY: &str = "MC4CAQAwBQYDK2VwBCIEIJ7kFBaOujmoz1gvBNEC+BeM2IX87FFB0xmISOZ/XO0c"; fn summary_with_prefix(summary: &str) -> String { format!("{SUMMARY_PREFIX}\n{summary}") } fn context_snapshot_options() -> ContextSnapshotOptions { ContextSnapshotOptions::default() .strip_capability_instructions() .render_mode(ContextSnapshotRenderMode::KindWithTextPrefix { max_chars: 64 }) } fn format_labeled_requests_snapshot( scenario: &str, sections: &[(&str, &responses::ResponsesRequest)], ) -> String { context_snapshot::format_labeled_requests_snapshot( scenario, sections, &context_snapshot_options(), ) } fn compacted_summary_only_output(summary: &str) -> Vec { vec![ResponseItem::Compaction { id: None, encrypted_content: summary_with_prefix(summary), internal_chat_message_metadata_passthrough: None, }] } fn test_codex() -> TestCodexBuilder { base_test_codex().with_config(|config| { config.update_plan_enabled = true; let _ = config.features.disable(Feature::RemoteCompactionV2); }) } fn unrestricted_user_turn(items: Vec) -> TurnInputRequest { TurnInputRequest::user_input(items).with_thread_settings(ThreadSettingsOverrides { approval_policy: Some(AskForApproval::Never), permission_profile: Some(PermissionProfile::Disabled), ..Default::default() }) } fn remote_realtime_test_codex_builder( realtime_server: &responses::WebSocketTestServer, ) -> TestCodexBuilder { let realtime_base_url = realtime_server.uri().to_string(); test_codex() .with_auth(CodexAuth::from_api_key("dummy")) .with_config(move |config| { config.experimental_realtime_ws_base_url = Some(realtime_base_url); }) } async fn start_remote_realtime_server() -> responses::WebSocketTestServer { start_websocket_server(vec![vec![ vec![json!({ "type": "session.updated", "session": { "id": "sess_remote_compact", "instructions": "backend prompt" } })], // Keep the websocket open after startup so routed transcript items during the test do not // exhaust the scripted responses and mark realtime inactive before the assertions run. vec![], vec![], vec![], vec![], vec![], vec![], vec![], vec![], ]]) .await } async fn start_realtime_conversation(codex: &codex_core::CodexThread) -> Result<()> { codex .submit(Op::RealtimeConversationStart(ConversationStartParams { client_managed_handoffs: false, delegation_ack_filler: None, flush_transcript_tail_on_session_end: false, codex_responses_as_items: false, codex_response_item_prefix: None, codex_response_handoff_mode: codex_protocol::protocol::CodexResponseHandoffMode::Thinking, codex_response_handoff_channel_prefixes: None, model: None, output_modality: RealtimeOutputModality::Audio, include_startup_context: true, initial_items: Vec::new(), realtime_start_instructions: None, realtime_end_instructions: None, prompt: Some(Some("backend prompt".to_string())), realtime_session_id: None, transport: None, version: None, voice: None, })) .await?; wait_for_event_match(codex, |msg| match msg { EventMsg::RealtimeConversationStarted(started) => Some(Ok(started.clone())), EventMsg::Error(err) => Some(Err(err.clone())), _ => None, }) .await .expect("conversation start failed"); wait_for_event_match(codex, |msg| match msg { EventMsg::RealtimeConversationRealtime(RealtimeConversationRealtimeEvent { payload: RealtimeEvent::SessionUpdated { realtime_session_id: session_id, .. }, }) => Some(session_id.clone()), _ => None, }) .await; Ok(()) } async fn close_realtime_conversation(codex: &codex_core::CodexThread) -> Result<()> { codex.submit(Op::RealtimeConversationClose).await?; wait_for_event_match(codex, |msg| match msg { EventMsg::RealtimeConversationClosed(closed) => Some(closed.clone()), _ => None, }) .await; Ok(()) } fn assert_request_contains_realtime_start(request: &responses::ResponsesRequest) { let body = request.body_json().to_string(); assert!( body.contains(""), "expected request to restate realtime instructions" ); } fn assert_request_contains_custom_realtime_start( request: &responses::ResponsesRequest, instructions: &str, ) { let body = request.body_json().to_string(); assert!( body.contains(""), "expected request to preserve the realtime wrapper" ); assert!( body.contains(instructions), "expected request to use custom realtime start instructions" ); assert!( !body.contains("Realtime conversation started."), "expected request to replace the default realtime start instructions" ); } fn assert_request_contains_realtime_end(request: &responses::ResponsesRequest) { let body = request.body_json().to_string(); assert!( body.contains(""), "expected request to restate realtime instructions" ); assert!( body.contains("Realtime conversation ended."), "expected request to use realtime end instructions" ); } async fn wait_for_turn_complete(codex: &codex_core::CodexThread) { wait_for_event_with_timeout( codex, |ev| matches!(ev, EventMsg::TurnComplete(_)), REMOTE_COMPACT_TURN_COMPLETE_TIMEOUT, ) .await; } fn amazon_bedrock_test_codex() -> TestCodexBuilder { let auth = CodexAuth::BedrockApiKey(BedrockApiKeyAuth { api_key: "bedrock-test-api-key".to_string(), region: "us-east-1".to_string(), }); test_codex() .with_auth(auth) .with_model(AMAZON_BEDROCK_GPT_5_5_MODEL_ID) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); config.model_provider = ModelProviderInfo { base_url: config.model_provider.base_url.clone(), ..ModelProviderInfo::create_amazon_bedrock_provider(/*aws*/ None) }; config.model_provider_id = AMAZON_BEDROCK_PROVIDER_ID.to_string(); }) } fn is_retained_user_message(item: &ResponseItem, retained_text: &str) -> bool { matches!( item, ResponseItem::Message { role, content, .. } if role == "user" && content.iter().any(|item| { matches!(item, ContentItem::InputText { text } if text == retained_text) }) ) } fn annotate_retained_user_in_rollout(path: &Path, retained_text: &str) -> Result<()> { let mut rollout = fs::read_to_string(path)? .lines() .filter(|line| !line.trim().is_empty()) .map(codex_rollout::parse_rollout_line) .collect::, _>>()?; rollout .iter_mut() .find_map(|line| match &mut line.item { RolloutItem::ResponseItem(envelope) if is_retained_user_message(&envelope.item, retained_text) => { Some(envelope) } _ => None, }) .context("persisted user response missing from rollout")? .metadata = Some(CodexHarnessMetadata::default()); let contents = rollout .iter() .map(serde_json::to_string) .collect::, _>>()? .join("\n"); fs::write(path, format!("{contents}\n"))?; Ok(()) } fn assert_compacted_user_metadata(path: &Path, retained_text: &str) -> Result<()> { let replacement_history = fs::read_to_string(path)? .lines() .filter(|line| !line.trim().is_empty()) .map(codex_rollout::parse_rollout_line) .collect::, _>>()? .into_iter() .rev() .find_map(|line| match line.item { RolloutItem::Compacted(compacted) => compacted.replacement_history, _ => None, }) .context("compacted replacement history missing")?; assert!( replacement_history.iter().any(|envelope| { is_retained_user_message(&envelope.item, retained_text) && envelope.metadata.is_some() }), "compacted user message should retain its aligned harness metadata" ); Ok(()) } fn assert_compact_request_omits_harness_metadata(request: &responses::ResponsesRequest) { for item in request.input() { assert!( item.get("metadata").is_none() && item.get("replacement_history_metadata").is_none(), "provider request must not receive harness history metadata: {item}" ); } } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn amazon_bedrock_manual_compaction_uses_v2_responses_endpoint() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_auto_env_builder(amazon_bedrock_test_codex()).await?; let response_mock = responses::mount_sse_sequence( harness.server(), vec![ sse(vec![ responses::ev_assistant_message("message-1", "before compaction"), responses::ev_completed("response-1"), ]), sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "BEDROCK_REMOTE_COMPACTED_SUMMARY", } }), responses::ev_completed("response-compact"), ]), sse(vec![ responses::ev_assistant_message("message-2", "after compaction"), responses::ev_completed("response-2"), ]), ], ) .await; harness.test().submit_turn("before compact").await?; harness.test().codex.submit(Op::Compact).await?; wait_for_turn_complete(&harness.test().codex).await; harness.test().submit_turn("after compact").await?; let response_requests = response_mock.requests(); assert_eq!(response_requests.len(), 3); assert!( response_requests .iter() .all(|request| request.path() == "/v1/responses") ); let compact_request = &response_requests[1]; assert_eq!( compact_request.header("authorization").as_deref(), Some("Bearer bedrock-test-api-key") ); assert_eq!( compact_request .header("x-amzn-mantle-client-agent") .as_deref(), Some("codex") ); assert_eq!( compact_request.body_json()["model"], AMAZON_BEDROCK_GPT_5_5_MODEL_ID ); assert_eq!( compact_request.inputs_of_type("compaction_trigger").len(), 1 ); assert!(response_requests[2].input().iter().any(|item| { item["type"] == "compaction" && item["encrypted_content"] == "BEDROCK_REMOTE_COMPACTED_SUMMARY" })); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_retains_metadata_from_resumed_history() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let retained_text = "annotated v2 user message"; let response_mock = responses::mount_sse_sequence( &server, vec![ sse(vec![ responses::ev_assistant_message("message-1", "before compaction"), responses::ev_completed("response-1"), ]), sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "ANNOTATED_V2_COMPACTION_SUMMARY", }, }), responses::ev_completed("response-compact"), ]), ], ) .await; let builder = || { test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); }) }; let initial = builder().build(&server).await?; initial.submit_turn(retained_text).await?; let home = initial.home.clone(); let rollout_path = initial .session_configured .rollout_path .clone() .context("rollout path")?; initial.codex.shutdown_and_wait().await?; annotate_retained_user_in_rollout(&rollout_path, retained_text)?; let resumed = builder() .resume(&server, home, rollout_path.clone()) .await?; resumed.codex.submit(Op::Compact).await?; wait_for_turn_complete(&resumed.codex).await; resumed.codex.shutdown_and_wait().await?; let requests = response_mock.requests(); let compact_request = requests.last().context("compact request")?; assert_eq!( compact_request.inputs_of_type("compaction_trigger").len(), 1 ); assert_compact_request_omits_harness_metadata(compact_request); assert_compacted_user_metadata(&rollout_path, retained_text)?; Ok(()) } #[test_case(false; "feature_disabled")] #[test_case(true; "feature_enabled")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_retains_only_client_developer_messages_when_enabled( enabled: bool, ) -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_auto_env_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(move |config| { config .features .enable(Feature::RemoteCompactionV2) .expect("remote compaction v2 should be configurable"); if enabled { config .features .enable(Feature::RetainClientDeveloperMessages) .expect("client developer retention should be configurable"); } }), ) .await?; let developer = |text: &str| ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: text.to_string(), }], phase: None, internal_chat_message_metadata_passthrough: None, }; let codex = &harness.test().codex; let rollout_path = codex.rollout_path().context("rollout path")?; codex .inject_response_items(vec![developer("INJECTED_CLIENT_DEVELOPER")]) .await?; let response_mock = responses::mount_sse_sequence( harness.server(), vec![ sse(vec![responses::ev_completed("before-compact")]), sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "CLIENT_RETENTION_SUMMARY", }, }), responses::ev_completed("compact"), ]), sse(vec![responses::ev_completed("after-compact")]), ], ) .await; harness.test().submit_turn("before compact").await?; codex.submit(Op::Compact).await?; wait_for_turn_complete(codex).await; harness.test().submit_turn("after compact").await?; let requests = response_mock.requests(); let follow_up = requests.last().context("follow-up request")?; assert_eq!( follow_up.body_contains_text("INJECTED_CLIENT_DEVELOPER"), enabled ); requests .iter() .for_each(assert_compact_request_omits_harness_metadata); codex.shutdown_and_wait().await?; let replacement_history = fs::read_to_string(&rollout_path)? .lines() .filter_map(|line| codex_rollout::parse_rollout_line(line).ok()) .filter_map(|line| match line.item { RolloutItem::Compacted(compacted) => compacted.replacement_history, _ => None, }) .next_back() .context("remote compaction should persist a checkpoint")?; let retained_client_developers = replacement_history .iter() .filter(|item| { item.metadata .as_ref() .is_some_and(|metadata| metadata.client_authored) }) .count(); assert_eq!(retained_client_developers, usize::from(enabled)); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_records_usage_before_output_validation() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_auto_env_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config .features .enable(Feature::RemoteCompactionV2) .expect("remote compaction v2 should be configurable"); }), ) .await?; let codex = &harness.test().codex; let rollout_path = codex.rollout_path().context("rollout path")?; responses::mount_sse_sequence( harness.server(), vec![ sse(vec![responses::ev_completed("before-compact")]), sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "FIRST_SUMMARY", }, }), json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "SECOND_SUMMARY", }, }), responses::ev_completed_with_tokens("invalid-compact", /*total_tokens*/ 8_200), ]), ], ) .await; harness.test().submit_turn("before compact").await?; codex.submit(Op::Compact).await?; wait_for_event(codex, |event| matches!(event, EventMsg::Error(_))).await; wait_for_event(codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex.shutdown_and_wait().await?; let record = fs::read_to_string(&rollout_path)? .lines() .filter_map(|line| codex_rollout::parse_rollout_line(line).ok()) .filter_map(|line| match line.item { RolloutItem::TokenUsageRecord(record) => Some(record), _ => None, }) .find(|record| record.response_id == "invalid-compact") .context("remote compaction usage record")?; assert_eq!(record.usage.total_tokens, 8_200); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn amazon_bedrock_automatic_compaction_uses_v2_responses_endpoint() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_auto_env_builder(amazon_bedrock_test_codex().with_config( |config| { config.model_auto_compact_token_limit = Some(200); }, )) .await?; let response_mock = responses::mount_sse_sequence( harness.server(), vec![ sse(vec![ responses::ev_assistant_message("message-1", "before automatic compaction"), responses::ev_completed_with_tokens("response-1", /*total_tokens*/ 500), ]), sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "BEDROCK_AUTOMATIC_REMOTE_COMPACTED_SUMMARY", } }), responses::ev_completed("response-compact"), ]), sse(vec![ responses::ev_assistant_message("message-2", "after automatic compaction"), responses::ev_completed("response-2"), ]), ], ) .await; harness .test() .submit_turn("before automatic compact") .await?; harness .test() .submit_turn("after automatic compact") .await?; let response_requests = response_mock.requests(); assert_eq!(response_requests.len(), 3); assert!( response_requests .iter() .all(|request| request.path() == "/v1/responses") ); let compact_request = &response_requests[1]; assert_eq!( compact_request.header("authorization").as_deref(), Some("Bearer bedrock-test-api-key") ); assert_eq!( compact_request .header("x-amzn-mantle-client-agent") .as_deref(), Some("codex") ); assert_eq!( compact_request.body_json()["model"], AMAZON_BEDROCK_GPT_5_5_MODEL_ID ); assert_eq!( compact_request.inputs_of_type("compaction_trigger").len(), 1 ); assert!(response_requests[2].input().iter().any(|item| { item["type"] == "compaction" && item["encrypted_content"] == "BEDROCK_AUTOMATIC_REMOTE_COMPACTED_SUMMARY" })); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_replaces_history_for_followups() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_history_mode(ThreadHistoryMode::Paginated) .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()), ) .await?; let codex = harness.test().codex.clone(); let session_id = harness.test().session_configured.session_id.to_string(); let thread_id = harness.test().session_configured.thread_id.to_string(); let responses_mock = responses::mount_sse_sequence( harness.server(), vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "FIRST_REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ]), ], ) .await; let compacted_history = vec![ResponseItem::Compaction { id: None, encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), internal_chat_message_metadata_passthrough: None, }]; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": compacted_history.clone() }), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let compact_request = compact_mock.single_request(); assert_eq!(compact_request.path(), "/v1/responses/compact"); assert_eq!( compact_request.header("chatgpt-account-id").as_deref(), Some("account_id") ); assert_eq!( compact_request.header("authorization").as_deref(), Some("Bearer Access Token") ); assert_eq!( compact_request.header("session-id").as_deref(), Some(session_id.as_str()) ); assert_eq!( compact_request.header("thread-id").as_deref(), Some(thread_id.as_str()) ); let compact_metadata: Value = serde_json::from_str( &compact_request .header("x-codex-turn-metadata") .expect("remote compact request should include turn metadata"), ) .expect("remote compact turn metadata should be valid json"); assert_eq!( compact_request.header("x-codex-installation-id").as_deref(), compact_metadata["installation_id"].as_str() ); assert!( compact_metadata["turn_id"] .as_str() .is_some_and(|id| !id.is_empty()), "remote compact turn metadata should include its turn id" ); assert_eq!( compact_metadata["request_kind"].as_str(), Some("compaction") ); assert_eq!(compact_metadata["sandbox_mode"].as_str(), Some("read-only")); assert_eq!( compact_metadata["window_id"].as_str(), compact_request.header("x-codex-window-id").as_deref() ); assert_eq!(compact_metadata["window_number"].as_u64(), Some(0)); assert!(compact_metadata["context_window_id"].as_str().is_some()); assert_eq!( compact_metadata["compaction"], json!({ "trigger": "manual", "reason": "user_requested", "implementation": "responses_compact", "phase": "standalone_turn", "strategy": "memento", }) ); let compact_body = compact_request.body_json(); assert_eq!( compact_body.get("model").and_then(|v| v.as_str()), Some(harness.test().session_configured.model.as_str()) ); let response_requests = responses_mock.requests(); let first_response_request = response_requests.first().expect("initial request missing"); let first_response_metadata: Value = serde_json::from_str( &first_response_request .header("x-codex-turn-metadata") .expect("initial request should include turn metadata"), ) .expect("initial turn metadata should be valid json"); assert_ne!( first_response_metadata["turn_id"], compact_metadata["turn_id"], "manual compaction should use its own turn id" ); assert_eq!( first_response_metadata["context_window_id"], compact_metadata["context_window_id"], "remote compaction should retain the active model-visible context window" ); assert_eq!( compact_body["tools"], first_response_request.body_json()["tools"], "compact requests should send the same tools payload as /v1/responses" ); assert_eq!( compact_body["parallel_tool_calls"], first_response_request.body_json()["parallel_tool_calls"], "compact requests should match /v1/responses parallel_tool_calls" ); assert_eq!( compact_body["reasoning"], first_response_request.body_json()["reasoning"], "compact requests should match /v1/responses reasoning" ); assert_eq!( compact_body["text"], first_response_request.body_json()["text"], "compact requests should match /v1/responses text controls" ); let compact_body_text = compact_body.to_string(); assert!( compact_body_text.contains("hello remote compact"), "expected compact request to include user history" ); assert!( compact_body_text.contains("FIRST_REMOTE_REPLY"), "expected compact request to include assistant history" ); let response_requests = responses_mock.requests(); let follow_up_request = response_requests.last().expect("follow-up request missing"); let follow_up_metadata: Value = serde_json::from_str( &follow_up_request .header("x-codex-turn-metadata") .expect("follow-up request should include turn metadata"), ) .expect("follow-up turn metadata should be valid json"); assert_eq!( follow_up_metadata["request_kind"].as_str(), Some("turn"), "regular requests after compaction should remain turn requests" ); assert!( follow_up_metadata.get("compaction").is_none(), "regular requests after compaction should not be marked as compact requests" ); assert_ne!( follow_up_metadata["turn_id"], compact_metadata["turn_id"], "the following user turn should not reuse a manual compact turn id" ); assert_eq!( follow_up_metadata["window_id"].as_str(), follow_up_request.header("x-codex-window-id").as_deref() ); assert_ne!( follow_up_metadata["window_id"], compact_metadata["window_id"], "the following user turn should use the new compacted context window" ); assert_ne!( follow_up_metadata["context_window_id"], compact_metadata["context_window_id"], "the following user turn should expose the new model-visible context window" ); let follow_up_body = follow_up_request.body_json().to_string(); assert!( follow_up_body.contains("\"type\":\"compaction\""), "expected follow-up request to use compacted history" ); assert!( follow_up_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), "expected follow-up request to include compaction summary item" ); assert!( !follow_up_body.contains("FIRST_REMOTE_REPLY"), "expected follow-up request to drop pre-compaction assistant messages" ); assert!( !follow_up_body.contains("hello remote compact"), "expected follow-up request to drop compacted-away user turns when remote output omits them" ); insta::assert_snapshot!( "remote_manual_compact_with_history_shapes", format_labeled_requests_snapshot( "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", &compact_request), ("Remote Post-Compaction History Layout", follow_up_request), ] ) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_uses_agent_identity_assertion() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex().with_auth(CodexAuth::AgentIdentity( AgentIdentityAuth::from_record( AgentIdentityAuthRecord { agent_runtime_id: "agent-runtime-compact".to_string(), agent_private_key: TEST_AGENT_IDENTITY_PRIVATE_KEY.to_string(), account_id: "account-compact".to_string(), chatgpt_user_id: "user-compact".to_string(), email: Some("agent@example.com".to_string()), plan_type: AccountPlanType::Plus, chatgpt_account_is_fedramp: false, task_id: Some("task-compact".to_string()), }, "https://auth.openai.com/api/accounts", &codex_login::test_support::transport_default_auth_route_config(), ) .await?, )), ) .await?; let codex = harness.test().codex.clone(); let _responses_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), ) .await; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": compacted_summary_only_output("COMPACTED") }), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; let compact_request = compact_mock.single_request(); assert_eq!(compact_request.path(), "/v1/responses/compact"); assert!( compact_request .header("authorization") .is_some_and(|value| value.starts_with("AgentAssertion ")), "compact request should use task-scoped AgentAssertion auth" ); assert_eq!( compact_request.header("chatgpt-account-id").as_deref(), Some("account-compact") ); let compact_body = compact_request.body_json(); let model = compact_body["model"] .as_str() .expect("missing request model"); assert_eq!( compact_request.header(X_CODEX_ROUTING_HINT_HEADER), Some(format!("model={model}")) ); Ok(()) } async fn assert_remote_manual_compact_request_parity( auth: CodexAuth, configured_service_tier: Option, expected_service_tier: Option<&str>, snapshot_name: &str, scenario: &str, ) -> Result<()> { let uses_codex_backend = auth.uses_codex_backend(); let mut builder = test_codex() .with_auth(auth) .with_pre_build_hook(allow_echo_commands); if let Some(service_tier) = configured_service_tier { builder = builder.with_config(move |config| { config.service_tier = Some(service_tier.request_value().to_string()); }); } let harness = TestCodexHarness::with_builder(builder).await?; let codex = harness.test().codex.clone(); let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=" .to_string(); let responses_mock = responses::mount_sse_sequence( harness.server(), vec![ responses::sse(vec![ responses::ev_assistant_message("turn-one-assistant", "TURN_ONE_ASSISTANT"), responses::ev_completed("turn-one-response"), ]), responses::sse(vec![ responses::ev_reasoning_item( "turn-two-reasoning", &["TURN_TWO_REASONING"], &["turn two raw content"], ), responses::ev_assistant_message("turn-two-assistant", "TURN_TWO_ASSISTANT"), responses::ev_completed("turn-two-response"), ]), responses::sse(vec![ responses::ev_function_call("turn-three-call", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed("turn-three-call-response"), ]), responses::sse(vec![ responses::ev_assistant_message("turn-three-assistant", "TURN_THREE_ASSISTANT"), responses::ev_completed("turn-three-final-response"), ]), responses::sse(vec![ responses::ev_exec_command_call( "turn-four-exec-command", "echo TURN_FOUR_LOCAL_SHELL", ), responses::ev_completed("turn-four-local-shell-response"), ]), responses::sse(vec![ responses::ev_assistant_message("turn-four-assistant", "TURN_FOUR_ASSISTANT"), responses::ev_completed("turn-four-final-response"), ]), responses::sse(vec![ responses::ev_reasoning_item( "turn-five-reasoning", &["TURN_FIVE_REASONING"], &["turn five raw content"], ), responses::ev_assistant_message("turn-five-assistant", "TURN_FIVE_ASSISTANT"), responses::ev_completed("turn-five-response"), ]), ], ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), "REMOTE_CACHE_TIER_SUMMARY", ) .await; codex .start_or_steer_turn(unrestricted_user_turn(vec![UserInput::Text { text: "TURN_ONE_USER".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![ UserInput::Text { text: "TURN_TWO_PREFIX".to_string(), text_elements: Vec::new(), }, UserInput::Text { text: "TURN_TWO_SUFFIX".to_string(), text_elements: Vec::new(), }, ])) .await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "TURN_THREE_TOOL_USER".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![ UserInput::Image { image_url, detail: None, }, UserInput::Text { text: "TURN_FOUR_IMAGE_USER".to_string(), text_elements: Vec::new(), }, ])) .await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "TURN_FIVE_USER".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let compact_turn_id = codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; let response_requests = responses_mock.requests(); assert_eq!( response_requests.len(), 7, "expected five turns with one unsupported tool continuation and one shell command continuation" ); assert_eq!( compact_mock.requests().len(), 1, "expected exactly one remote compact request" ); let normal_request = response_requests .last() .cloned() .expect("last turn request missing"); let compact_request = compact_mock.single_request(); let normal_body = normal_request.body_json(); let compact_body = compact_request.body_json(); let compact_metadata: Value = serde_json::from_str( &compact_request .header("x-codex-turn-metadata") .expect("compaction turn metadata"), )?; assert_eq!(compact_metadata["turn_id"], compact_turn_id); assert_eq!(compact_metadata["root_turn_id"], compact_turn_id); let expected_routing_hint = |body: &Value| { if !uses_codex_backend { return None; } let model = body["model"].as_str().expect("missing request model"); match body.get("service_tier").and_then(Value::as_str) { Some(tier) => Some(format!("model={model};tier={tier}")), None => Some(format!("model={model}")), } }; assert_eq!( normal_request.header(X_CODEX_ROUTING_HINT_HEADER), expected_routing_hint(&normal_body) ); assert_eq!( compact_request.header(X_CODEX_ROUTING_HINT_HEADER), expected_routing_hint(&compact_body) ); let mut expected_compact_body_without_input = normal_body.clone(); let expected_compact_object = expected_compact_body_without_input .as_object_mut() .expect("responses request body should be an object"); for field in [ "input", "client_metadata", "include", "store", "stream", "tool_choice", ] { expected_compact_object.remove(field); } if expected_service_tier.is_none() { expected_compact_object.remove("service_tier"); } let mut compact_body_without_input = compact_body.clone(); compact_body_without_input .as_object_mut() .expect("compact request body should be an object") .remove("input"); let canonical_compact_body_without_input = canonical_json(&compact_body_without_input); let canonical_expected_compact_body_without_input = canonical_json(&expected_compact_body_without_input); assert_eq!( json!({ "compact_body_without_input": canonical_compact_body_without_input, "expected_compact_body_without_input": canonical_expected_compact_body_without_input, "prompt_cache_key_matches_responses": compact_body["prompt_cache_key"] == normal_body["prompt_cache_key"], "prompt_cache_key_present": compact_body["prompt_cache_key"].is_string(), "service_tier": compact_body.get("service_tier").and_then(Value::as_str), }), json!({ "compact_body_without_input": canonical_expected_compact_body_without_input, "expected_compact_body_without_input": canonical_expected_compact_body_without_input, "prompt_cache_key_matches_responses": true, "prompt_cache_key_present": true, "service_tier": expected_service_tier, }), "compact requests should carry the same shared request fields as /responses" ); insta::assert_snapshot!( snapshot_name, context_snapshot::format_request_body_diff_snapshot( scenario, "Last Normal /responses Request", &normal_request, "Remote /responses/compact Request", &compact_request, &ContextSnapshotOptions::default().strip_response_item_ids(), ) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_manual_compact_api_auth_omits_service_tier_and_reuses_prompt_cache_key() -> Result<()> { skip_if_no_network!(Ok(())); assert_remote_manual_compact_request_parity( CodexAuth::from_api_key("dummy"), Some(ServiceTier::Fast), /*expected_service_tier*/ None, "remote_manual_compact_api_auth_prompt_cache_key_request_diff", "After five varied API-key-auth turns, remote manual compaction omits service_tier, reuses prompt_cache_key, and still omits responses-only fields.", ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_manual_compact_chatgpt_auth_reuses_service_tier_and_prompt_cache_key() -> Result<()> { skip_if_no_network!(Ok(())); assert_remote_manual_compact_request_parity( CodexAuth::create_dummy_chatgpt_auth_for_testing(), Some(ServiceTier::Fast), Some("priority"), "remote_manual_compact_chatgpt_auth_service_tier_prompt_cache_key_request_diff", "After five varied ChatGPT-auth turns, remote manual compaction reuses service_tier and prompt_cache_key while omitting responses-only fields.", ) .await?; Ok(()) } #[test_case(None; "default_trims_images")] #[test_case(Some(false); "disabled_preserves_images")] #[test_case(Some(true); "enabled_trims_images")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_charges_retained_images_to_token_budget( image_budget_enabled: Option, ) -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_auto_env_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(move |config| { let _ = config.features.enable(Feature::RemoteCompactionV2); let _ = config.features.enable(Feature::UnifiedImageBudget); if let Some(enabled) = image_budget_enabled { let _ = config .features .set_enabled(Feature::CompactionImageBudget, enabled); } }), ) .await?; let codex = &harness.test().codex; // Each original-detail image costs 10,000 estimated patch tokens. let image_inputs = (1..=8) .map(|number| { let image = image::ImageBuffer::from_pixel( /*width*/ 3200, /*height*/ 3200, image::Luma([number as u8]), ); let mut bytes = std::io::Cursor::new(Vec::new()); image.write_to(&mut bytes, image::ImageFormat::Png)?; Ok(UserInput::Image { image_url: format!( "data:image/png;base64,{}", BASE64_STANDARD.encode(bytes.get_ref()) ), detail: Some(codex_protocol::models::ImageDetail::Original), }) }) .collect::>>()?; let mut input = image_inputs[..7].to_vec(); input.push(UserInput::Text { text: "Compare these images".to_string(), text_elements: Vec::new(), }); let initial_mock = mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("initial", "done"), responses::ev_completed("initial"), ]), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(input)) .await?; wait_for_turn_complete(codex).await; let initial_request = initial_mock.single_request(); let prepared_images = initial_request.message_input_image_urls("user"); assert_eq!(prepared_images.len(), 7); for cycle in 1..=2 { let compact_mock = mount_sse_once( harness.server(), sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "IMAGE_BUDGET_SUMMARY" }, }), responses::ev_completed("compact-images"), ]), ) .await; codex.submit(Op::Compact).await?; wait_for_turn_complete(codex).await; let compact_request = compact_mock.single_request(); assert_eq!(compact_request.path(), "/v1/responses"); assert_eq!( compact_request.inputs_of_type("compaction_trigger").len(), 1 ); let follow_up_mock = mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("after", "done"), responses::ev_completed("after"), ]), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(codex).await; let follow_up = follow_up_mock.single_request(); assert_eq!( follow_up.inputs_of_type("compaction")[0]["encrypted_content"], "IMAGE_BUDGET_SUMMARY" ); let dropped = if image_budget_enabled.unwrap_or(true) { cycle } else { 0 }; let mut expected_images = prepared_images[dropped..].to_vec(); if cycle == 2 { let UserInput::Image { image_url, .. } = &image_inputs[7] else { unreachable!() }; expected_images.push(image_url.clone()); } assert_eq!(follow_up.message_input_image_urls("user"), expected_images); assert!( follow_up .message_input_texts("user") .iter() .any(|text| text == "Compare these images") ); if cycle == 1 { let append_mock = mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("append", "done"), responses::ev_completed("append"), ]), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![image_inputs[7].clone()])) .await?; wait_for_turn_complete(codex).await; let _ = append_mock.single_request(); } } Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); }), ) .await?; let image_url = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII="; let user_notice = "retained user image"; let tool_notice = "discarded tool image"; let unlisted_notice = "discarded developer notice"; let developer_message = |text| { json!({ "type": "message", "role": "developer", "content": [{ "type": "input_text", "text": text }] }) }; let initial_history = [ json!({ "type": "message", "role": "user", "content": [ { "type": "input_text", "text": "retained image source" }, { "type": "input_image", "image_url": image_url } ] }), developer_message(user_notice), developer_message(unlisted_notice), json!({ "type": "function_call", "name": "view_image", "arguments": "{}", "call_id": "image-call" }), json!({ "type": "function_call_output", "call_id": "image-call", "output": [{ "type": "input_image", "image_url": image_url }] }), developer_message(tool_notice), ] .into_iter() .map(|item| { serde_json::from_value::(item) .map(|item| RolloutItem::ResponseItem(item.into())) }) .collect::>>()?; let codex = harness .test() .thread_manager .start_thread(StartThreadOptions { initial_history: InitialHistory::Forked(initial_history), ..StartThreadOptions::new(harness.test().config.clone()) }) .await? .thread; let responses_mock = responses::mount_sse_sequence( harness.server(), vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "FIRST_REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), responses::sse(vec![ responses::ev_assistant_message("m-agent", "DELEGATED_TASK_REPLY"), responses::ev_completed("resp-agent"), ]), responses::sse(vec![ responses::ev_assistant_message("m-descendant", "DESCENDANT_FOLLOWUP_REPLY"), responses::ev_completed("resp-descendant"), ]), responses::sse(vec![ serde_json::json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "ENCRYPTED_CONTEXT_COMPACTION_SUMMARY", } }), responses::ev_completed("resp-compact"), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ]), ], ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex .submit(Op::InterAgentCommunication { communication: InterAgentCommunication::new( AgentPath::root().join("child").expect("valid child path"), AgentPath::root(), Vec::new(), "Message Type: MESSAGE\nTask name: /root\nSender: /root/child\nPayload:\nchild progress" .to_string(), /*trigger_turn*/ false, ), start_options: Default::default(), }) .await?; codex .submit(Op::InterAgentCommunication { communication: InterAgentCommunication::new( AgentPath::root().join("child").expect("valid child path"), AgentPath::root(), Vec::new(), "Message Type: FINAL_ANSWER\nTask name: /root\nSender: /root/child\nPayload:\nchild completion".to_string(), /*trigger_turn*/ false, ), start_options: Default::default(), }) .await?; let delegated_task_ciphertext = format!("delegated compact task{}", "x".repeat(40_000)); codex .submit(Op::InterAgentCommunication { communication: InterAgentCommunication::new_encrypted( AgentPath::root(), AgentPath::root().join("worker").expect("valid worker path"), Vec::new(), delegated_task_ciphertext.clone(), /*trigger_turn*/ true, ), start_options: Default::default(), }) .await?; wait_for_turn_complete(&codex).await; let descendant_followup_ciphertext = "descendant follow-up task"; let worker_path = AgentPath::root().join("worker").expect("valid worker path"); codex .submit(Op::InterAgentCommunication { communication: InterAgentCommunication::new_encrypted( worker_path.join("child").expect("valid grandchild path"), worker_path, Vec::new(), descendant_followup_ciphertext.to_string(), /*trigger_turn*/ true, ), start_options: Default::default(), }) .await?; wait_for_turn_complete(&codex).await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let response_requests = responses_mock.requests(); let compact_request = &response_requests[3]; let item_create_time = |request: &responses::ResponsesRequest, text: &str| { request .input() .into_iter() .find(|item| { item["content"].as_array().is_some_and(|content| { content.iter().any(|part| { part["text"].as_str() == Some(text) || part["encrypted_content"].as_str() == Some(text) }) }) }) .and_then(|item| { item.pointer("/internal_chat_message_metadata_passthrough/create_time") .cloned() }) .expect("matching message should include a creation timestamp") }; let original_user_create_time = item_create_time(&response_requests[0], "hello remote compact"); let delegated_task_create_time = item_create_time(&response_requests[1], &delegated_task_ciphertext); assert!( compact_request .inputs_of_type("agent_message") .iter() .any(|item| item["content"][1]["encrypted_content"].as_str() == Some(delegated_task_ciphertext.as_str())), "expected v2 compaction input to include the encrypted delegated task" ); assert!( compact_request .inputs_of_type("agent_message") .iter() .any(|item| item["content"][1]["encrypted_content"].as_str() == Some(descendant_followup_ciphertext)), "expected v2 compaction input to include the descendant-authored follow-up task" ); assert!( compact_request .inputs_of_type("agent_message") .iter() .any(|item| item.to_string().contains("child progress")), "expected v2 compaction input to include the child progress update" ); assert!( compact_request .inputs_of_type("agent_message") .iter() .any(|item| item.to_string().contains("child completion")), "expected v2 compaction input to include the child completion" ); assert!( compact_request .header("x-codex-beta-features") .as_deref() .is_some_and(|value| value .split(',') .any(|feature| feature == "remote_compaction_v2")), "expected compact request to advertise the remote_compaction_v2 beta feature" ); assert_eq!(compact_request.path(), "/v1/responses"); let compact_metadata: Value = serde_json::from_str( &compact_request .header("x-codex-turn-metadata") .expect("v2 compact request should include turn metadata"), ) .expect("v2 compact turn metadata should be valid json"); assert_eq!( compact_metadata["request_kind"].as_str(), Some("compaction") ); assert_eq!( compact_metadata["window_id"].as_str(), compact_request.header("x-codex-window-id").as_deref() ); assert_eq!( compact_request.body_json()["client_metadata"]["x-codex-window-id"].as_str(), compact_metadata["window_id"].as_str() ); assert_eq!( compact_metadata["compaction"], json!({ "trigger": "manual", "reason": "user_requested", "implementation": "responses_compaction_v2", "phase": "standalone_turn", "strategy": "memento", }) ); let compact_body = compact_request.body_json().to_string(); assert!( compact_body.contains("\"type\":\"compaction_trigger\""), "expected v2 compaction request to include the compaction_trigger item" ); assert!( !compact_body.contains("ENCRYPTED_CONTEXT_COMPACTION_SUMMARY"), "expected v2 compaction trigger item to omit encrypted_content" ); let follow_up_request = response_requests.last().expect("follow-up request missing"); assert_eq!( item_create_time(follow_up_request, "hello remote compact"), original_user_create_time ); assert_eq!( item_create_time(follow_up_request, &delegated_task_ciphertext), delegated_task_create_time ); assert!( item_create_time(follow_up_request, "after compact") .as_f64() .is_some_and(|create_time| create_time > 0.0) ); assert!( follow_up_request .inputs_of_type("agent_message") .iter() .any(|item| item["content"][1]["encrypted_content"].as_str() == Some(delegated_task_ciphertext.as_str())), "expected v2 follow-up request to retain the encrypted delegated task" ); assert!( follow_up_request .inputs_of_type("agent_message") .iter() .any(|item| item["content"][1]["encrypted_content"].as_str() == Some(descendant_followup_ciphertext)), "expected v2 follow-up request to retain the descendant-authored follow-up task" ); assert!( follow_up_request .inputs_of_type("agent_message") .iter() .all(|item| !item.to_string().contains("child progress")), "expected v2 follow-up request to omit the child progress update" ); assert!( follow_up_request .inputs_of_type("agent_message") .iter() .all(|item| !item.to_string().contains("child completion")), "expected v2 follow-up request to omit the child completion" ); let follow_up_body = follow_up_request.body_json().to_string(); assert!( follow_up_body.contains("\"type\":\"compaction\""), "expected follow-up request to preserve the compaction item" ); assert!( follow_up_body.contains("ENCRYPTED_CONTEXT_COMPACTION_SUMMARY"), "expected follow-up request to include the compaction payload" ); assert!( follow_up_body.contains("hello remote compact"), "expected v2 follow-up request to preserve retained original user messages" ); assert!( follow_up_request.input().windows(2).any(|items| { items[0]["role"] == "user" && items[0]["content"][0]["text"] == "retained image source" && items[1]["role"] == "developer" && items[1]["content"][0]["text"] == user_notice }), "expected v2 compaction to retain the user image and its adjacent resize notice" ); assert!( !follow_up_body.contains(unlisted_notice), "expected v2 compaction to drop unlisted developer notices" ); assert!( !follow_up_body.contains(tool_notice), "expected v2 compaction to drop the resize notice with its tool output" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_history_mode(ThreadHistoryMode::Paginated) .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); config.model_provider.request_max_retries = Some(0); config.model_provider.stream_max_retries = Some(2); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_response_sequence( harness.server(), vec![ responses::sse_response(responses::sse(vec![ responses::ev_assistant_message("m1", "FIRST_REMOTE_REPLY"), responses::ev_completed("resp-1"), ])), ResponseTemplate::new(500).set_body_string("first compact open failed"), responses::sse_response(responses::sse(vec![serde_json::json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "FAILED_COMPACT_SUMMARY", } })])), responses::sse_response(responses::sse(vec![ serde_json::json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "RETRIED_COMPACT_SUMMARY", } }), responses::ev_completed("resp-compact-retry"), ])), responses::sse_response(responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ])), ], ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let response_requests = responses_mock.requests(); assert_eq!( 5, response_requests.len(), "expected initial turn, failed open, failed stream, compact retry, and follow-up turn" ); for compact_request in &response_requests[1..=3] { assert_eq!("/v1/responses", compact_request.path()); let compact_metadata: Value = serde_json::from_str( &compact_request .header("x-codex-turn-metadata") .expect("v2 compact request should include turn metadata"), )?; assert_eq!(compact_metadata["window_number"].as_u64(), Some(0)); assert!(compact_metadata["context_window_id"].as_str().is_some()); assert!( compact_request .body_json() .to_string() .contains("\"type\":\"compaction_trigger\""), "expected v2 compaction request to include the compaction_trigger item" ); } let follow_up_request = response_requests.last().expect("follow-up request missing"); let follow_up_body = follow_up_request.body_json().to_string(); assert!( follow_up_body.contains("RETRIED_COMPACT_SUMMARY"), "expected follow-up request to include the retried compaction payload" ); assert!( !follow_up_body.contains("FAILED_COMPACT_SUMMARY"), "expected failed compaction attempt output to be discarded" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_v2_accepts_additional_output_items_before_compaction() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_sse_sequence( harness.server(), vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "FIRST_REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), responses::sse(vec![ responses::ev_assistant_message("m-compact-noise", "IGNORED_COMPACT_REPLY"), serde_json::json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "ENCRYPTED_CONTEXT_COMPACTION_SUMMARY", } }), responses::ev_completed("resp-compact"), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ]), ], ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let response_requests = responses_mock.requests(); let follow_up_request = response_requests.last().expect("follow-up request missing"); let follow_up_body = follow_up_request.body_json().to_string(); assert!( follow_up_body.contains("\"type\":\"compaction\""), "expected follow-up request to preserve the compaction item" ); assert!( follow_up_body.contains("ENCRYPTED_CONTEXT_COMPACTION_SUMMARY"), "expected follow-up request to include the compaction payload" ); assert!( !follow_up_body.contains("IGNORED_COMPACT_REPLY"), "expected follow-up request to ignore unrelated output items from the compaction stream" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_filters_deferred_dynamic_tools() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let mut test = builder.build(&server).await?; let hidden_tool = "hidden_dynamic_tool"; let visible_tool = "visible_dynamic_tool"; let input_schema = json!({ "type": "object", "properties": {}, "additionalProperties": false, }); let dynamic_tools = vec![DynamicToolSpec::Namespace(DynamicToolNamespaceSpec { name: "codex_app".to_string(), description: "Codex app tools.".to_string(), tools: vec![ DynamicToolNamespaceTool::Function(DynamicToolFunctionSpec { name: hidden_tool.to_string(), description: "Hidden until discovered.".to_string(), input_schema: input_schema.clone(), defer_loading: true, }), DynamicToolNamespaceTool::Function(DynamicToolFunctionSpec { name: visible_tool.to_string(), description: "Visible immediately.".to_string(), input_schema, defer_loading: false, }), ], })]; let new_thread = test .thread_manager .start_thread(StartThreadOptions { dynamic_tools, ..StartThreadOptions::new(test.config.clone()) }) .await?; test.codex = new_thread.thread; test.session_configured = new_thread.session_configured; let codex = test.codex.clone(); let responses_mock = mount_sse_once( &server, sse(vec![ responses::ev_assistant_message("m1", "FIRST_REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), ) .await; let compact_mock = responses::mount_compact_json_once( &server, serde_json::json!({ "output": compacted_summary_only_output("compact summary"), }), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; let first_response_body = responses_mock.single_request().body_json(); let compact_body = compact_mock.single_request().body_json(); assert_eq!( compact_body["tools"], first_response_body["tools"], "compact requests should send the same model-visible tools payload as /v1/responses" ); assert_tools_payload_does_not_defer(&first_response_body); assert_tools_payload_does_not_defer(&compact_body); assert_eq!( namespace_child_tool_names(&first_response_body, "codex_app"), vec![visible_tool.to_string()] ); assert_eq!( namespace_child_tool_names(&compact_body, "codex_app"), vec![visible_tool.to_string()] ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_does_not_charge_inline_audio_payload_as_text() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let call_id = "audio-call"; let tool_name = "recording"; let audio_url = pcm_wav_data_url(/*sample_count*/ 300_000); let responses_mock = responses::mount_sse_sequence( &server, vec![ sse(vec![ responses::ev_response_created("resp-1"), responses::ev_function_call_with_namespace(call_id, "codex_app", tool_name, "{}"), responses::ev_completed_with_tokens("resp-1", /*total_tokens*/ 100), ]), sse(vec![ responses::ev_response_created("resp-2"), responses::ev_assistant_message("msg-1", "done"), responses::ev_completed_with_tokens("resp-2", /*total_tokens*/ 200), ]), ], ) .await; let compact_mock = responses::mount_compact_json_once( &server, json!({ "output": compacted_summary_only_output("compact summary") }), ) .await; let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_model_info_override("gpt-5.5", |model_info| { model_info.input_modalities.push(InputModality::Audio); }) .with_config(|config| { config.model_context_window = Some(50_000); }); let mut test = builder.build(&server).await?; let dynamic_tool = DynamicToolSpec::Namespace(DynamicToolNamespaceSpec { name: "codex_app".to_string(), description: "Audio tools.".to_string(), tools: vec![DynamicToolNamespaceTool::Function( DynamicToolFunctionSpec { name: tool_name.to_string(), description: "Returns a recording.".to_string(), input_schema: json!({ "type": "object", "properties": {}, "additionalProperties": false, }), defer_loading: false, }, )], }); let new_thread = test .thread_manager .start_thread(StartThreadOptions { dynamic_tools: vec![dynamic_tool], ..StartThreadOptions::new(test.config.clone()) }) .await?; test.codex = new_thread.thread; test.session_configured = new_thread.session_configured; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "Return a recording".to_string(), text_elements: Vec::new(), }])) .await?; let EventMsg::DynamicToolCallRequest(request) = wait_for_event(&test.codex, |event| { matches!(event, EventMsg::DynamicToolCallRequest(_)) }) .await else { unreachable!("event guard guarantees DynamicToolCallRequest"); }; test.codex .submit(Op::DynamicToolResponse { id: request.call_id, response: DynamicToolResponse { content_items: vec![DynamicToolCallOutputContentItem::InputAudio { audio_url: audio_url.clone(), }], success: true, }, }) .await?; wait_for_turn_complete(&test.codex).await; test.codex.submit(Op::Compact).await?; wait_for_turn_complete(&test.codex).await; assert_eq!(responses_mock.requests().len(), 2); let output = compact_mock .single_request() .function_call_output(call_id) .get("output") .cloned() .expect("compact request should retain the dynamic tool output"); assert_eq!( serde_json::from_value::(output)?, FunctionCallOutputPayload { body: FunctionCallOutputBody::ContentItems(vec![ FunctionCallOutputContentItem::InputAudio { audio_url }, ]), success: None, } ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_runs_automatically() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_pre_build_hook(allow_echo_commands), ) .await?; let codex = harness.test().codex.clone(); let session_id = harness.test().session_configured.session_id.to_string(); let thread_id = harness.test().session_configured.thread_id.to_string(); let initial_request = mount_sse_once( harness.server(), sse(vec![ responses::ev_exec_command_call("m1", "echo 'hi'"), responses::ev_completed_with_tokens("resp-1", /*total_tokens*/ 100000000), // over token limit ]), ) .await; let responses_mock = mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ]), ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), "REMOTE_COMPACTED_SUMMARY", ) .await; codex .start_or_steer_turn(unrestricted_user_turn(vec![UserInput::Text { text: "hello remote compact".into(), text_elements: Vec::new(), }])) .await?; let message = wait_for_event_match(&codex, |event| match event { EventMsg::ContextCompacted(_) => Some(true), _ => None, }) .await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert!(message); assert_eq!(compact_mock.requests().len(), 1); assert_eq!( compact_mock .single_request() .header("session-id") .as_deref(), Some(session_id.as_str()) ); assert_eq!( compact_mock.single_request().header("thread-id").as_deref(), Some(thread_id.as_str()) ); let compact_metadata: Value = serde_json::from_str( &compact_mock .single_request() .header("x-codex-turn-metadata") .expect("auto remote compact request should include turn metadata"), ) .expect("auto remote compact turn metadata should be valid json"); assert_eq!( compact_metadata["request_kind"].as_str(), Some("compaction") ); assert_eq!( compact_metadata["compaction"], json!({ "trigger": "auto", "reason": "context_limit", "implementation": "responses_compact", "phase": "mid_turn", "strategy": "memento", }) ); let initial_metadata: Value = serde_json::from_str( &initial_request .single_request() .header("x-codex-turn-metadata") .expect("initial request should include turn metadata"), ) .expect("initial turn metadata should be valid json"); assert_eq!( initial_metadata["turn_id"], compact_metadata["turn_id"], "automatic mid-turn compaction should keep the current turn id" ); assert_eq!( initial_metadata["window_id"], compact_metadata["window_id"], "automatic mid-turn compaction summarizes the current context window" ); let follow_up_request = responses_mock.single_request(); let follow_up_metadata: Value = serde_json::from_str( &follow_up_request .header("x-codex-turn-metadata") .expect("post-compaction continuation should include turn metadata"), ) .expect("post-compaction turn metadata should be valid json"); assert_eq!( follow_up_metadata["request_kind"].as_str(), Some("turn"), "post-compaction continuation should be a regular request" ); assert!(follow_up_metadata.get("compaction").is_none()); assert_eq!( follow_up_metadata["turn_id"], compact_metadata["turn_id"], "automatic mid-turn continuation should keep the current turn id" ); assert_ne!( follow_up_metadata["window_id"], compact_metadata["window_id"], "post-compaction continuation should use the next context window" ); let follow_up_body = follow_up_request.body_json().to_string(); assert!(follow_up_body.contains("REMOTE_COMPACTED_SUMMARY")); Ok(()) } #[cfg_attr(target_os = "windows", ignore)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_trims_function_call_history_to_fit_context_window() -> Result<()> { skip_if_no_network!(Ok(())); let first_user_message = "turn with retained shell call"; let second_user_message = "turn with trimmed shell call"; let retained_call_id = "retained-call"; let trimmed_call_id = "trimmed-call"; let retained_command = "echo retained-shell-output"; let trimmed_command = "yes x | head -n 3000"; let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_context_window = Some(2_000); config.model_auto_compact_token_limit = Some(200_000); }), ) .await?; let codex = harness.test().codex.clone(); responses::mount_sse_sequence( harness.server(), vec![ sse(vec![ responses::ev_exec_command_call(retained_call_id, retained_command), responses::ev_completed("retained-call-response"), ]), sse(vec![ responses::ev_assistant_message("retained-assistant", "retained complete"), responses::ev_completed("retained-final-response"), ]), sse(vec![ responses::ev_exec_command_call(trimmed_call_id, trimmed_command), responses::ev_completed("trimmed-call-response"), ]), ], ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), "REMOTE_COMPACT_SUMMARY", ) .await; codex.submit(Op::Compact).await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let compact_request = compact_mock.single_request(); let user_messages = compact_request.message_input_texts("user"); assert!( user_messages .iter() .any(|message| message == first_user_message), "expected compact request to retain earlier user history" ); assert!( user_messages .iter() .any(|message| message == second_user_message), "expected compact request to retain the user boundary message" ); assert!( compact_request.has_function_call(retained_call_id) && compact_request .function_call_output_text(retained_call_id) .is_some(), "expected compact request to keep the older function call/result pair" ); assert!( compact_request.has_function_call(trimmed_call_id), "expected compact request to retain the trailing function call" ); assert_eq!( compact_request.function_call_output_text(trimmed_call_id), Some(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()), "expected compact request to rewrite the trailing function call output past the boundary" ); assert_eq!( compact_request.inputs_of_type("function_call").len(), 2, "expected both function calls after rewriting the trailing output" ); assert_eq!( compact_request.inputs_of_type("function_call_output").len(), 2, "expected both function call outputs after rewriting the trailing output" ); Ok(()) } #[cfg_attr(target_os = "windows", ignore)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_rewrites_multiple_trailing_function_call_outputs() -> Result<()> { skip_if_no_network!(Ok(())); let first_user_message = "turn with retained shell call"; let second_user_message = "turn with parallel shell calls"; let retained_call_id = "retained-call"; let first_trimmed_call_id = "first-trimmed-call"; let second_trimmed_call_id = "second-trimmed-call"; let retained_command = "echo retained-shell-output"; let first_trimmed_command = "yes x | head -n 3000"; let second_trimmed_command = "yes y | head -n 3000"; let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_context_window = Some(2_000); config.model_auto_compact_token_limit = Some(200_000); }), ) .await?; let codex = harness.test().codex.clone(); responses::mount_sse_sequence( harness.server(), vec![ sse(vec![ responses::ev_exec_command_call(retained_call_id, retained_command), responses::ev_completed("retained-call-response"), ]), sse(vec![ responses::ev_assistant_message("retained-assistant", "retained complete"), responses::ev_completed("retained-final-response"), ]), sse(vec![ responses::ev_exec_command_call(first_trimmed_call_id, first_trimmed_command), responses::ev_exec_command_call(second_trimmed_call_id, second_trimmed_command), responses::ev_completed("parallel-call-response"), ]), ], ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), "REMOTE_COMPACT_SUMMARY", ) .await; codex.submit(Op::Compact).await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let compact_request = compact_mock.single_request(); assert!( compact_request.has_function_call(retained_call_id) && compact_request .function_call_output_text(retained_call_id) .is_some(), "expected compact request to keep the older function call/result pair" ); assert!( compact_request.has_function_call(first_trimmed_call_id) && compact_request.has_function_call(second_trimmed_call_id), "expected compact request to retain both trailing parallel function calls" ); assert_eq!( compact_request.function_call_output_text(first_trimmed_call_id), Some(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()), "expected compact request to rewrite the first trailing function call output" ); assert_eq!( compact_request.function_call_output_text(second_trimmed_call_id), Some(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()), "expected compact request to rewrite the second trailing function call output" ); assert_eq!( compact_request.inputs_of_type("function_call").len(), 3, "expected all function calls after rewriting trailing outputs" ); assert_eq!( compact_request.inputs_of_type("function_call_output").len(), 3, "expected all function call outputs after rewriting trailing outputs" ); Ok(()) } #[cfg_attr(target_os = "windows", ignore)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auto_remote_compact_trims_function_call_history_to_fit_context_window() -> Result<()> { skip_if_no_network!(Ok(())); let first_user_message = "turn with retained shell call"; let second_user_message = "turn with trimmed shell call"; let retained_call_id = "retained-call"; let trimmed_call_id = "trimmed-call"; let retained_command = "echo retained-shell-output"; let trimmed_command = "yes x | head -n 3000"; let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_context_window = Some(2_000); config.model_auto_compact_token_limit = Some(200_000); }), ) .await?; let codex = harness.test().codex.clone(); responses::mount_sse_sequence( harness.server(), vec![ sse(vec![ responses::ev_exec_command_call(retained_call_id, retained_command), responses::ev_completed_with_tokens( "retained-call-response", /*total_tokens*/ 100, ), ]), sse(vec![ responses::ev_assistant_message("retained-assistant", "retained complete"), responses::ev_completed("retained-final-response"), ]), sse(vec![ responses::ev_exec_command_call(trimmed_call_id, trimmed_command), responses::ev_completed_with_tokens( "trimmed-call-response", /*total_tokens*/ 100, ), ]), sse(vec![responses::ev_completed_with_tokens( "trimmed-final-response", /*total_tokens*/ 500_000, )]), ], ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), "REMOTE_AUTO_COMPACT_SUMMARY", ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "turn that triggers auto compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert_eq!( compact_mock.requests().len(), 1, "expected exactly one remote compact request" ); let compact_request = compact_mock.single_request(); let user_messages = compact_request.message_input_texts("user"); assert!( user_messages .iter() .any(|message| message == first_user_message), "expected compact request to retain earlier user history" ); assert!( user_messages .iter() .any(|message| message == second_user_message), "expected compact request to retain the user boundary message" ); assert!( compact_request.has_function_call(retained_call_id) && compact_request .function_call_output_text(retained_call_id) .is_some(), "expected compact request to keep the older function call/result pair" ); assert!( compact_request.has_function_call(trimmed_call_id), "expected compact request to retain the trailing function call" ); assert_eq!( compact_request.function_call_output_text(trimmed_call_id), Some(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()), "expected compact request to rewrite the trailing function call output past the boundary" ); assert_eq!( compact_request.inputs_of_type("function_call").len(), 2, "expected both function calls after rewriting the trailing output" ); assert_eq!( compact_request.inputs_of_type("function_call_output").len(), 2, "expected both function call outputs after rewriting the trailing output" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_trims_tool_search_output_to_empty_tools_array() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let search_call_id = "tool-search-1"; let tool_name = "oversized_dynamic_tool"; let tool_description = format!( "Oversized deferred tool for remote compaction. {}", "x".repeat(20_000) ); let _responses_mock = mount_sse_once( &server, sse(vec![ responses::ev_response_created("resp-1"), responses::ev_tool_search_call( search_call_id, &json!({ "query": "oversized deferred tool", "limit": 8, }), ), responses::ev_completed("resp-1"), ]), ) .await; let input_schema = json!({ "type": "object", "properties": { "mode": { "type": "string" }, }, "required": ["mode"], "additionalProperties": false, }); let dynamic_tool = DynamicToolSpec::Namespace(DynamicToolNamespaceSpec { name: "codex_app".to_string(), description: "Codex app tools.".to_string(), tools: vec![DynamicToolNamespaceTool::Function( DynamicToolFunctionSpec { name: tool_name.to_string(), description: tool_description, input_schema, defer_loading: true, }, )], }); let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { configure_search_capable_model(config); config.model_context_window = Some(2_000); }); let mut test = builder.build(&server).await?; let new_thread = test .thread_manager .start_thread(StartThreadOptions { dynamic_tools: vec![dynamic_tool], ..StartThreadOptions::new(test.config.clone()) }) .await?; test.codex = new_thread.thread; test.session_configured = new_thread.session_configured; let codex = test.codex.clone(); codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "Find the oversized deferred tool".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let compact_mock = responses::mount_compact_user_history_with_summary_once(&server, "REMOTE_COMPACT_SUMMARY") .await; codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; let compact_request = compact_mock.single_request(); let compact_tools = compact_request .tool_search_output(search_call_id) .get("tools") .and_then(Value::as_array) .cloned() .unwrap_or_default(); assert!( compact_request .inputs_of_type("tool_search_output") .iter() .any(|item| item.get("call_id").and_then(Value::as_str) == Some(search_call_id)), "expected compact request to retain the tool_search_output item" ); assert!( compact_tools.is_empty(), "expected compact request to rewrite trailing tool_search output to an empty tools array" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn auto_remote_compact_failure_stops_agent_loop() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(120); }), ) .await?; let codex = harness.test().codex.clone(); mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("initial-assistant", "initial turn complete"), responses::ev_completed_with_tokens("initial-response", /*total_tokens*/ 500_000), ]), ) .await; let first_compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": "invalid compact payload shape" }), ) .await; let post_compact_turn_mock = mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("post-compact-assistant", "should not run"), responses::ev_completed("post-compact-response"), ]), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "turn that exceeds token threshold".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "turn that triggers auto compact".into(), text_elements: Vec::new(), }])) .await?; let error_message = wait_for_event_match(&codex, |event| match event { EventMsg::Error(err) => Some(err.message.clone()), _ => None, }) .await; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert!( error_message.contains("Error running remote compact task"), "expected remote compact task error prefix, got {error_message}" ); assert_eq!( first_compact_mock.requests().len(), 1, "expected first remote compact attempt with incoming items" ); assert!( post_compact_turn_mock.requests().is_empty(), "expected agent loop to stop after compaction failure" ); insta::assert_snapshot!( "remote_pre_turn_compaction_failure_shapes", format_labeled_requests_snapshot( "Remote pre-turn auto-compaction parse failure: compaction request excludes the incoming user message and the turn stops.", &[( "Remote Compaction Request (Incoming User Excluded)", &first_compact_mock.single_request() ),] ) ); Ok(()) } #[cfg_attr(target_os = "windows", ignore)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_trim_estimate_uses_session_base_instructions() -> Result<()> { skip_if_no_network!(Ok(())); let first_user_message = "turn with baseline shell call"; let second_user_message = "turn with trailing shell call"; let baseline_retained_call_id = "baseline-retained-call"; let baseline_trailing_call_id = "baseline-trailing-call"; let override_retained_call_id = "override-retained-call"; let override_trailing_call_id = "override-trailing-call"; let retained_command = "printf retained-shell-output"; let trailing_command = "printf '%020000d' 0"; let baseline_harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_context_window = Some(200_000); }), ) .await?; let baseline_codex = baseline_harness.test().codex.clone(); responses::mount_sse_sequence( baseline_harness.server(), vec![ sse(vec![ responses::ev_exec_command_call(baseline_retained_call_id, retained_command), responses::ev_completed("baseline-retained-call-response"), ]), sse(vec![ responses::ev_assistant_message("baseline-retained-assistant", "retained complete"), responses::ev_completed("baseline-retained-final-response"), ]), sse(vec![ responses::ev_exec_command_call(baseline_trailing_call_id, trailing_command), responses::ev_completed("baseline-trailing-call-response"), ]), sse(vec![responses::ev_completed( "baseline-trailing-final-response", )]), ], ) .await; baseline_codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&baseline_codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; baseline_codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&baseline_codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let baseline_compact_mock = responses::mount_compact_user_history_with_summary_once( baseline_harness.server(), "REMOTE_BASELINE_SUMMARY", ) .await; baseline_codex.submit(Op::Compact).await?; wait_for_event(&baseline_codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let baseline_compact_request = baseline_compact_mock.single_request(); assert!( baseline_compact_request.has_function_call(baseline_retained_call_id), "expected baseline compact request to retain older function call history" ); assert!( baseline_compact_request.has_function_call(baseline_trailing_call_id), "expected baseline compact request to retain trailing function call history" ); let baseline_input_tokens = estimate_compact_input_tokens(&baseline_compact_request); let baseline_payload_tokens = estimate_compact_payload_tokens(&baseline_compact_request); let override_base_instructions = format!( "{}\nREMOTE_BASE_INSTRUCTIONS_OVERRIDE {}", baseline_compact_request.instructions_text(), "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)); assert!( pretrim_override_estimate > override_context_window, "expected override instructions to push pre-trim estimate past the context window" ); let override_harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config({ let override_base_instructions = override_base_instructions.clone(); move |config| { config.model_context_window = Some(override_context_window); config.base_instructions = Some(override_base_instructions); } }), ) .await?; let override_codex = override_harness.test().codex.clone(); responses::mount_sse_sequence( override_harness.server(), vec![ sse(vec![ responses::ev_exec_command_call(override_retained_call_id, retained_command), responses::ev_completed("override-retained-call-response"), ]), sse(vec![ responses::ev_assistant_message("override-retained-assistant", "retained complete"), responses::ev_completed("override-retained-final-response"), ]), sse(vec![ responses::ev_exec_command_call(override_trailing_call_id, trailing_command), responses::ev_completed("override-trailing-call-response"), ]), sse(vec![responses::ev_completed( "override-trailing-final-response", )]), ], ) .await; override_codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: first_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&override_codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; override_codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: second_user_message.into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&override_codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let override_compact_mock = responses::mount_compact_user_history_with_summary_once( override_harness.server(), "REMOTE_OVERRIDE_SUMMARY", ) .await; override_codex.submit(Op::Compact).await?; wait_for_event(&override_codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; let override_compact_request = override_compact_mock.single_request(); assert_eq!( override_compact_request.instructions_text(), override_base_instructions ); assert!( override_compact_request.has_function_call(override_retained_call_id), "expected remote compact request to preserve older function call history" ); assert!( override_compact_request.has_function_call(override_trailing_call_id), "expected remote compact request to preserve trailing function call history with override instructions" ); assert_eq!( override_compact_request.function_call_output_text(override_trailing_call_id), Some(CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE.to_string()), "expected remote compact request to rewrite trailing function call output with override instructions" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_manual_compact_emits_context_compaction_items() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()), ) .await?; let codex = harness.test().codex.clone(); mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("m1", "REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), "REMOTE_COMPACTED_SUMMARY", ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "manual remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex.submit(Op::Compact).await?; let mut started_item = None; let mut completed_item = None; let mut legacy_event = false; let mut saw_turn_complete = false; while !saw_turn_complete || started_item.is_none() || completed_item.is_none() || !legacy_event { let event = codex.next_event().await.unwrap(); match event.msg { EventMsg::ItemStarted(ItemStartedEvent { item: TurnItem::ContextCompaction(item), .. }) => { started_item = Some(item); } EventMsg::ItemCompleted(ItemCompletedEvent { item: TurnItem::ContextCompaction(item), .. }) => { completed_item = Some(item); } EventMsg::ContextCompacted(_) => { legacy_event = true; } EventMsg::TurnComplete(_) => { saw_turn_complete = true; } _ => {} } } let started_item = started_item.expect("context compaction item started"); let completed_item = completed_item.expect("context compaction item completed"); assert_eq!(started_item.id, completed_item.id); assert!(legacy_event); assert_eq!(compact_mock.requests().len(), 1); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_manual_compact_failure_emits_task_error_event() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()), ) .await?; let codex = harness.test().codex.clone(); mount_sse_once( harness.server(), sse(vec![ responses::ev_assistant_message("m1", "REMOTE_REPLY"), responses::ev_completed("resp-1"), ]), ) .await; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": "invalid compact payload shape" }), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "manual remote compact".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; codex.submit(Op::Compact).await?; let error_message = wait_for_event_match(&codex, |event| match event { EventMsg::Error(err) => Some(err.message.clone()), _ => None, }) .await; assert!( error_message.contains("Error running remote compact task"), "expected remote compact task error prefix, got {error_message}" ); assert!( error_message.contains("invalid compact payload shape") || error_message.contains("invalid type: string"), "expected invalid compact payload details, got {error_message}" ); wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] // TODO(ccunningham): Re-enable after the follow-up compaction behavior PR lands. // Current main behavior for rollout replacement-history persistence is known-incorrect. #[ignore = "behavior change covered in follow-up compaction PR"] async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()), ) .await?; let codex = harness.test().codex.clone(); let rollout_path = harness .test() .session_configured .rollout_path .clone() .expect("rollout path"); let responses_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m1", "COMPACT_BASELINE_REPLY"), responses::ev_completed("resp-1"), ]), ) .await; let compacted_history = vec![ ResponseItem::Compaction { id: None, encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), internal_chat_message_metadata_passthrough: None, }, ResponseItem::Message { id: None, role: "assistant".to_string(), content: vec![ContentItem::OutputText { text: "COMPACTED_ASSISTANT_NOTE".to_string(), }], phase: None, internal_chat_message_metadata_passthrough: None, }, ]; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": compacted_history.clone() }), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "needs compaction".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex.submit(Op::Compact).await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex.submit(Op::Shutdown).await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; assert_eq!(responses_mock.requests().len(), 1); assert_eq!(compact_mock.requests().len(), 1); let rollout_text = fs::read_to_string(&rollout_path)?; let mut saw_compacted_history = false; for line in rollout_text .lines() .map(str::trim) .filter(|l| !l.is_empty()) { let Ok(entry) = codex_rollout::parse_rollout_line(line) else { continue; }; if let RolloutItem::Compacted(compacted) = entry.item && compacted.message.is_empty() && let Some(replacement_history) = compacted.replacement_history.as_ref() { let has_compaction_item = replacement_history.iter().any(|item| { matches!( &item.item, ResponseItem::Compaction { encrypted_content, .. } if encrypted_content == "ENCRYPTED_COMPACTION_SUMMARY" ) }); let has_compacted_assistant_note = replacement_history.iter().any(|item| { matches!( &item.item, ResponseItem::Message { role, content, .. } if role == "assistant" && content.iter().any(|part| matches!( part, ContentItem::OutputText { text } if text == "COMPACTED_ASSISTANT_NOTE" )) ) }); let has_permissions_developer_message = replacement_history.iter().any(|item| { matches!( &item.item, ResponseItem::Message { role, content, .. } if role == "developer" && content.iter().any(|part| matches!( part, ContentItem::InputText { text } if text.contains("") )) ) }); if has_compaction_item && has_compacted_assistant_note { assert!( !has_permissions_developer_message, "manual remote compact rollout replacement history should not inject permissions context" ); saw_compacted_history = true; break; } } } assert!( saw_compacted_history, "expected rollout to persist remote compaction history" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_and_resume_refresh_stale_developer_instructions() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let stale_developer_message = "STALE_DEVELOPER_INSTRUCTIONS_SHOULD_BE_REMOVED"; let managed_requirements = CloudConfigBundleFixture::loader_with_enterprise_requirement( r#"additional_developer_instructions = "MANAGED_DEVELOPER_INSTRUCTIONS""#, ); let managed_message = "\nMANAGED_DEVELOPER_INSTRUCTIONS\n"; let mut start_builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_cloud_config_bundle(managed_requirements.clone()); let initial = start_builder.build(&server).await?; let home = initial.home.clone(); let rollout_path = initial .session_configured .rollout_path .clone() .expect("rollout path"); let responses_mock = responses::mount_sse_sequence( &server, vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "BASELINE_REPLY"), responses::ev_completed("resp-1"), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ]), responses::sse(vec![ responses::ev_assistant_message("m3", "AFTER_RESUME_REPLY"), responses::ev_completed("resp-3"), ]), ], ) .await; let compacted_history = vec![ ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: stale_developer_message.to_string(), }], phase: None, internal_chat_message_metadata_passthrough: None, }, ResponseItem::Compaction { id: None, encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), internal_chat_message_metadata_passthrough: None, }, ]; let compact_mock = responses::mount_compact_json_once( &server, serde_json::json!({ "output": compacted_history }), ) .await; initial .codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "start remote compact flow".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; initial.codex.submit(Op::Compact).await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; initial .codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact in same session".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&initial.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; initial.codex.submit(Op::Shutdown).await?; wait_for_event(&initial.codex, |ev| { matches!(ev, EventMsg::ShutdownComplete) }) .await; let mut resume_builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_cloud_config_bundle(managed_requirements); let resumed = resume_builder.resume(&server, home, rollout_path).await?; resumed .codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after resume".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&resumed.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!(requests.len(), 3, "expected three model requests"); for request in &requests { assert_eq!( request .message_input_texts("developer") .into_iter() .filter(|message| message.contains("")) .collect::>(), vec![managed_message.to_string()], "Each model request should contain exactly one managed developer-instructions block, including after compaction and resume." ); } let after_compact_request = &requests[1]; let after_resume_request = &requests[2]; let after_compact_body = after_compact_request.body_json().to_string(); assert!( !after_compact_body.contains(stale_developer_message), "stale developer instructions should be removed immediately after compaction" ); assert!( after_compact_body.contains(""), "fresh developer instructions should be present after compaction" ); assert!( after_compact_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), "compaction item should be present after compaction" ); let after_resume_body = after_resume_request.body_json().to_string(); assert!( !after_resume_body.contains(stale_developer_message), "stale developer instructions should be removed after resume" ); assert!( after_resume_body.contains(""), "fresh developer instructions should be present after resume" ); assert!( after_resume_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), "compaction item should persist after resume" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_compact_refreshes_stale_developer_instructions_without_resume() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let stale_developer_message = "STALE_DEVELOPER_INSTRUCTIONS_SHOULD_BE_REMOVED"; let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); let test = builder.build(&server).await?; let responses_mock = responses::mount_sse_sequence( &server, vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "BASELINE_REPLY"), responses::ev_completed("resp-1"), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed("resp-2"), ]), ], ) .await; let compacted_history = vec![ ResponseItem::Message { id: None, role: "developer".to_string(), content: vec![ContentItem::InputText { text: stale_developer_message.to_string(), }], phase: None, internal_chat_message_metadata_passthrough: None, }, ResponseItem::Compaction { id: None, encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), internal_chat_message_metadata_passthrough: None, }, ]; let compact_mock = responses::mount_compact_json_once( &server, serde_json::json!({ "output": compacted_history }), ) .await; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "start remote compact flow".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex.submit(Op::Compact).await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "after compact in same session".into(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!(requests.len(), 2, "expected two model requests"); let after_compact_body = requests[1].body_json().to_string(); assert!( !after_compact_body.contains(stale_developer_message), "stale developer instructions should be removed immediately after compaction" ); assert!( after_compact_body.contains(""), "fresh developer instructions should be present after compaction" ); assert!( after_compact_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), "compaction item should be present after compaction" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_pre_turn_compaction_restates_realtime_start() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let realtime_server = start_remote_realtime_server().await; let mut builder = remote_realtime_test_codex_builder(&realtime_server).with_config(|config| { config.model_auto_compact_token_limit = Some(200); }); let test = builder.build(&server).await?; let responses_mock = responses::mount_sse_sequence( &server, vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_FIRST_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_SECOND_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ], ) .await; let compact_mock = responses::mount_compact_json_once( &server, serde_json::json!({ "output": compacted_summary_only_output( "REMOTE_PRETURN_REALTIME_STILL_ACTIVE_SUMMARY" ) }), ) .await; start_realtime_conversation(test.codex.as_ref()).await?; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!(requests.len(), 2, "expected two model requests"); let compact_request = compact_mock.single_request(); let post_compact_request = &requests[1]; assert_request_contains_realtime_start(post_compact_request); insta::assert_snapshot!( "remote_pre_turn_compaction_restates_realtime_start_shapes", format_labeled_requests_snapshot( "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", &compact_request), ( "Remote Post-Compaction History Layout", post_compact_request ), ] ) ); close_realtime_conversation(test.codex.as_ref()).await?; realtime_server.shutdown().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_request_uses_custom_experimental_realtime_start_instructions() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let realtime_server = start_remote_realtime_server().await; let custom_instructions = "custom realtime start instructions"; let mut builder = remote_realtime_test_codex_builder(&realtime_server).with_config({ let custom_instructions = custom_instructions.to_string(); move |config| { config.experimental_realtime_start_instructions = Some(custom_instructions); } }); let test = builder.build(&server).await?; let responses_mock = responses::mount_sse_once( &server, responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_FIRST_REPLY"), responses::ev_completed("r1"), ]), ) .await; start_realtime_conversation(test.codex.as_ref()).await?; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_request_contains_custom_realtime_start( &responses_mock.single_request(), custom_instructions, ); close_realtime_conversation(test.codex.as_ref()).await?; realtime_server.shutdown().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn active_realtime_does_not_diff_changed_start_instructions_after_resume() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let initial_realtime_server = start_remote_realtime_server().await; let initial_instructions = "initial custom realtime start instructions"; let mut initial_builder = remote_realtime_test_codex_builder(&initial_realtime_server) .with_config({ let initial_instructions = initial_instructions.to_string(); move |config| { config.experimental_realtime_start_instructions = Some(initial_instructions); } }); let initial = initial_builder.build(&server).await?; let home = initial.home.clone(); let rollout_path = initial .session_configured .rollout_path .clone() .expect("rollout path"); let responses_mock = responses::mount_sse_sequence( &server, vec![ responses::sse(vec![responses::ev_completed("r1")]), responses::sse(vec![responses::ev_completed("r2")]), ], ) .await; start_realtime_conversation(initial.codex.as_ref()).await?; initial.submit_turn("USER_ONE").await?; close_realtime_conversation(initial.codex.as_ref()).await?; initial.codex.submit(Op::Shutdown).await?; wait_for_event(&initial.codex, |ev| { matches!(ev, EventMsg::ShutdownComplete) }) .await; initial_realtime_server.shutdown().await; let resumed_realtime_server = start_remote_realtime_server().await; let changed_instructions = "changed custom realtime start instructions"; let mut resume_builder = remote_realtime_test_codex_builder(&resumed_realtime_server) .with_config({ let changed_instructions = changed_instructions.to_string(); move |config| { config.experimental_realtime_start_instructions = Some(changed_instructions); } }); let resumed = resume_builder.resume(&server, home, rollout_path).await?; start_realtime_conversation(resumed.codex.as_ref()).await?; resumed.submit_turn("USER_TWO").await?; let requests = responses_mock.requests(); assert_eq!(requests.len(), 2, "expected one request per session"); assert_request_contains_custom_realtime_start(&requests[0], initial_instructions); let resumed_body = requests[1].body_json().to_string(); assert!( resumed_body.contains(initial_instructions), "expected resumed history to retain the original realtime instructions" ); assert!( !resumed_body.contains(changed_instructions), "did not expect an active-to-active instruction change to emit a diff" ); close_realtime_conversation(resumed.codex.as_ref()).await?; resumed_realtime_server.shutdown().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_manual_compact_restates_realtime_start() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let realtime_server = start_remote_realtime_server().await; let mut builder = remote_realtime_test_codex_builder(&realtime_server); let test = builder.build(&server).await?; let responses_mock = responses::mount_sse_sequence( &server, vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_FIRST_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 60), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_SECOND_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ], ) .await; let compact_mock = responses::mount_compact_json_once( &server, serde_json::json!({ "output": compacted_summary_only_output( "REMOTE_MANUAL_REALTIME_STILL_ACTIVE_SUMMARY" ) }), ) .await; start_realtime_conversation(test.codex.as_ref()).await?; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex.submit(Op::Compact).await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!(requests.len(), 2, "expected two model requests"); let compact_request = compact_mock.single_request(); let post_compact_request = &requests[1]; assert_request_contains_realtime_start(post_compact_request); insta::assert_snapshot!( "remote_manual_compact_restates_realtime_start_shapes", format_labeled_requests_snapshot( "Remote manual /compact while realtime remains active: the next regular turn restates realtime-start instructions after compaction clears the baseline.", &[ ("Remote Compaction Request", &compact_request), ( "Remote Post-Compaction History Layout", post_compact_request ), ] ) ); close_realtime_conversation(test.codex.as_ref()).await?; realtime_server.shutdown().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_mid_turn_compaction_does_not_restate_realtime_end() -> Result<()> { skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; let realtime_server = start_remote_realtime_server().await; let mut builder = remote_realtime_test_codex_builder(&realtime_server).with_config(|config| { config.model_auto_compact_token_limit = Some(200); }); let test = builder.build(&server).await?; let responses_mock = responses::mount_sse_sequence( &server, vec![ responses::sse(vec![ responses::ev_assistant_message("setup", "REMOTE_SETUP_REPLY"), responses::ev_completed_with_tokens("setup-response", /*total_tokens*/ 60), ]), responses::sse(vec![ responses::ev_function_call("call-remote-mid-turn", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_MID_TURN_FINAL_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ], ) .await; let compact_mock = responses::mount_compact_json_once( &server, serde_json::json!({ "output": compacted_summary_only_output( "REMOTE_MID_TURN_REALTIME_CLOSED_SUMMARY" ) }), ) .await; start_realtime_conversation(test.codex.as_ref()).await?; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "SETUP_USER".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; close_realtime_conversation(test.codex.as_ref()).await?; test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&test.codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!(requests.len(), 3, "expected three model requests"); let second_turn_request = &requests[1]; let compact_request = compact_mock.single_request(); let post_compact_request = &requests[2]; assert_request_contains_realtime_end(second_turn_request); assert!( !post_compact_request .body_json() .to_string() .contains(""), "did not expect post-compaction history to restate realtime instructions once the current turn had already established an inactive baseline" ); insta::assert_snapshot!( "remote_mid_turn_compaction_does_not_restate_realtime_end_shapes", format_labeled_requests_snapshot( "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", second_turn_request), ("Remote Compaction Request", &compact_request), ( "Remote Post-Compaction History Layout", post_compact_request ), ] ) ); realtime_server.shutdown().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] // TODO(ccunningham): Update once remote pre-turn compaction includes incoming user input. async fn snapshot_request_shape_remote_pre_turn_compaction_including_incoming_user_message() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_sse_sequence( harness.server(), vec![ responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_FIRST_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 60), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_SECOND_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 500), ]), responses::sse(vec![ responses::ev_assistant_message("m3", "REMOTE_FINAL_REPLY"), responses::ev_completed_with_tokens("r3", /*total_tokens*/ 80), ]), ], ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), &summary_with_prefix("REMOTE_PRE_TURN_SUMMARY"), ) .await; for user in ["USER_ONE", "USER_TWO", "USER_THREE"] { if user == "USER_THREE" { core_test_support::submit_thread_settings( &codex, ThreadSettingsOverrides { environments: Some(local_selections( test_path_buf(PRETURN_CONTEXT_DIFF_CWD).abs(), )), ..Default::default() }, ) .await?; } codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: user.to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; } assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!( requests.len(), 3, "expected user, user, and post-compact turn" ); let compact_request = compact_mock.single_request(); insta::assert_snapshot!( "remote_pre_turn_compaction_including_incoming_shapes", format_labeled_requests_snapshot( "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", &compact_request), ("Remote Post-Compaction History Layout", &requests[2]), ] ) ); assert_eq!( requests[2] .message_input_texts("user") .iter() .filter(|text| text.as_str() == "USER_THREE") .count(), 1, "post-compaction request should contain incoming user exactly once from runtime append" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_pre_turn_compaction_strips_incoming_model_switch() -> Result<()> { skip_if_no_network!(Ok(())); let previous_model = "gpt-5.4"; let next_model = "gpt-5.2"; let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_model(previous_model) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let initial_turn_request_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m1", "BEFORE_SWITCH_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ]), ) .await; let post_compact_turn_request_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_SWITCH_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), &summary_with_prefix("REMOTE_SWITCH_SUMMARY"), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "BEFORE_SWITCH_USER".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; core_test_support::submit_thread_settings( &codex, ThreadSettingsOverrides { model: Some(next_model.to_string()), ..Default::default() }, ) .await?; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "AFTER_SWITCH_USER".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!( compact_mock.requests().len(), 1, "expected a single remote pre-turn compaction request" ); assert_eq!( initial_turn_request_mock.requests().len(), 1, "expected initial turn request" ); assert_eq!( post_compact_turn_request_mock.requests().len(), 1, "expected post-compaction follow-up request" ); let initial_turn_request = initial_turn_request_mock.single_request(); let compact_request = compact_mock.single_request(); let post_compact_turn_request = post_compact_turn_request_mock.single_request(); let compact_body = compact_request.body_json().to_string(); assert!( !compact_body.contains("AFTER_SWITCH_USER"), "current behavior excludes incoming user from the pre-turn remote compaction request" ); assert!( !compact_body.contains(""), "pre-turn remote compaction request should strip incoming model-switch update item" ); let follow_up_body = post_compact_turn_request.body_json().to_string(); assert!( follow_up_body.contains("BEFORE_SWITCH_USER"), "post-compaction follow-up should preserve older user messages when they fit" ); assert!( follow_up_body.contains("AFTER_SWITCH_USER"), "post-compaction follow-up should preserve incoming user message via runtime append" ); assert!( follow_up_body.contains(""), "post-compaction follow-up should include the model-switch update item" ); insta::assert_snapshot!( "remote_pre_turn_compaction_strips_incoming_model_switch_shapes", format_labeled_requests_snapshot( "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)", &initial_turn_request), ("Remote Compaction Request", &compact_request), ( "Remote Post-Compaction History Layout", &post_compact_turn_request ), ] ) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] // TODO(ccunningham): Update once remote pre-turn compaction context-overflow handling includes // incoming user input and emits richer oversized-input messaging. async fn snapshot_request_shape_remote_pre_turn_compaction_context_window_exceeded() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_sse_sequence( harness.server(), vec![responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_FIRST_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ])], ) .await; let compact_mock = responses::mount_compact_response_once( harness.server(), ResponseTemplate::new(400).set_body_json(serde_json::json!({ "error": { "code": "context_length_exceeded", "message": "Your input exceeds the context window of this model. Please adjust your input and try again." } })), ) .await; let post_compact_turn_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_POST_COMPACT_SHOULD_NOT_RUN"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), }])) .await?; let error_message = wait_for_event_match(&codex, |event| match event { EventMsg::Error(err) => Some(err.message.clone()), _ => None, }) .await; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!( requests.len(), 1, "expected no post-compaction follow-up turn request after compact failure" ); assert!( post_compact_turn_mock.requests().is_empty(), "expected turn to stop after compaction failure" ); let include_attempt_request = compact_mock.single_request(); insta::assert_snapshot!( "remote_pre_turn_compaction_context_window_exceeded_shapes", format_labeled_requests_snapshot( "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)", &include_attempt_request ),] ) ); assert!( error_message.to_lowercase().contains("context window"), "expected context window failure to surface, got {error_message}" ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_pre_turn_compact_response_seeds_turn_state() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_response_sequence( harness.server(), vec![ responses::sse_response(responses::sse(vec![ responses::ev_assistant_message("m1", "BEFORE_COMPACT_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ])), responses::sse_response(responses::sse(vec![ responses::ev_assistant_message("m2", "AFTER_COMPACT_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ])), ], ) .await; let compact_mock = responses::mount_compact_response_once( harness.server(), ResponseTemplate::new(200) .insert_header("content-type", "application/json") .insert_header(TURN_STATE_HEADER, "compact-state") .set_body_json(json!({ "output": compacted_summary_only_output("PRE_TURN_COMPACT_SUMMARY"), })), ) .await; // Phase 1: the first turn raises usage above the pre-turn compact threshold. // Phase 2: the next turn compacts before sampling and establishes turn state. for text in ["BEFORE_COMPACT_USER", "AFTER_COMPACT_USER"] { codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: text.to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; } // Phase 3: compact starts empty, and its returned state is sent to the first sample. assert_eq!( compact_mock.single_request().header(TURN_STATE_HEADER), None ); let requests = responses_mock.requests(); assert_eq!(requests.len(), 2); assert_eq!(requests[0].header(TURN_STATE_HEADER), None); assert_eq!( requests[1].header(TURN_STATE_HEADER).as_deref(), Some("compact-state") ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_mid_turn_compact_v1_sends_turn_state_over_http() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_response_sequence( harness.server(), vec![ responses::sse_response(responses::sse(vec![ responses::ev_function_call("call-before-compact", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ])) .insert_header(TURN_STATE_HEADER, "sampling-state"), responses::sse_response(responses::sse(vec![ responses::ev_function_call("call-after-compact", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ])) .insert_header(TURN_STATE_HEADER, "continuation-state"), responses::sse_response(responses::sse(vec![ responses::ev_assistant_message("m1", "FINAL_REPLY"), responses::ev_completed_with_tokens("r3", /*total_tokens*/ 80), ])), ], ) .await; let compact_mock = responses::mount_compact_response_once( harness.server(), ResponseTemplate::new(200) .insert_header("content-type", "application/json") .insert_header(TURN_STATE_HEADER, "compact-state") .set_body_json(json!({ "output": compacted_summary_only_output("MID_TURN_COMPACT_SUMMARY"), })), ) .await; // Phase 1: sampling mints state and crosses the token limit with a pending tool follow-up. codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "RUN_WITH_MID_TURN_COMPACT".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; // Phase 2: v1 compact receives the state established by sampling. let compact_request = compact_mock.single_request(); assert_eq!(compact_request.path(), "/v1/responses/compact"); assert_eq!( compact_request.header(TURN_STATE_HEADER).as_deref(), Some("sampling-state") ); // Phase 3: every remaining request keeps replaying that first value. let requests = responses_mock.requests(); assert_eq!(requests.len(), 3); assert_eq!(requests[0].header(TURN_STATE_HEADER), None); assert_eq!( requests[1].header(TURN_STATE_HEADER).as_deref(), Some("sampling-state") ); assert_eq!( requests[2].header(TURN_STATE_HEADER).as_deref(), Some("sampling-state") ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_mid_turn_compact_v2_sends_turn_state_over_http() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_response_sequence( harness.server(), vec![ responses::sse_response(responses::sse(vec![ responses::ev_function_call("call-before-compact", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ])) .insert_header(TURN_STATE_HEADER, "sampling-state"), responses::sse_response(responses::sse(vec![ json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "V2_COMPACT_SUMMARY", } }), responses::ev_completed("r-compact"), ])) .insert_header(TURN_STATE_HEADER, "compact-state"), responses::sse_response(responses::sse(vec![ responses::ev_function_call("call-after-compact", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ])) .insert_header(TURN_STATE_HEADER, "continuation-state"), responses::sse_response(responses::sse(vec![ responses::ev_assistant_message("m1", "FINAL_REPLY"), responses::ev_completed_with_tokens("r3", /*total_tokens*/ 80), ])), ], ) .await; // Phase 1: sampling mints state and schedules inline v2 compaction. codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "RUN_WITH_MID_TURN_COMPACT_V2".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&codex).await; let requests = responses_mock.requests(); assert_eq!(requests.len(), 4); assert!( requests .iter() .all(|request| request.path() == "/v1/responses") ); assert_eq!(requests[0].header(TURN_STATE_HEADER), None); // Phase 2: the v2 compaction request replays the state already established by sampling. assert!( requests[1] .body_json() .to_string() .contains("\"type\":\"compaction_trigger\"") ); assert_eq!( requests[1].header(TURN_STATE_HEADER).as_deref(), Some("sampling-state") ); // Phase 3: later response headers do not replace the first value in the OnceLock. assert_eq!( requests[2].header(TURN_STATE_HEADER).as_deref(), Some("sampling-state") ); assert_eq!( requests[3].header(TURN_STATE_HEADER).as_deref(), Some("sampling-state") ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn remote_mid_turn_compact_v2_sends_turn_state_over_websocket() -> Result<()> { skip_if_no_network!(Ok(())); let server = start_websocket_server(vec![vec![ vec![ responses::ev_response_created("warm-1"), responses::ev_completed("warm-1"), ], vec![ json!({ "type": "response.metadata", "headers": {(TURN_STATE_HEADER): "sampling-state"}, }), responses::ev_function_call("call-before-compact", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ], vec![ json!({ "type": "response.metadata", "headers": {(TURN_STATE_HEADER): "compact-state"}, }), json!({ "type": "response.output_item.done", "item": { "type": "compaction", "encrypted_content": "V2_WS_COMPACT_SUMMARY", } }), responses::ev_completed("r-compact"), ], vec![ json!({ "type": "response.metadata", "headers": {(TURN_STATE_HEADER): "continuation-state"}, }), responses::ev_function_call("call-after-compact", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ], vec![ responses::ev_assistant_message("m1", "FINAL_REPLY"), responses::ev_completed_with_tokens("r3", /*total_tokens*/ 80), ], ]]) .await; let mut builder = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { let _ = config.features.enable(Feature::RemoteCompactionV2); config.model_auto_compact_token_limit = Some(200); }); let test = builder.build_with_websocket_server(&server).await?; // Phase 1: startup prewarm stays empty, then WebSocket sampling mints state and schedules // inline v2 compaction. test.codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "RUN_WITH_WS_MID_TURN_COMPACT_V2".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_turn_complete(&test.codex).await; let requests = server.single_connection(); assert_eq!(requests.len(), 5); assert_eq!(requests[0].body_json()["generate"].as_bool(), Some(false)); // Phase 2: the v2 compact request replays the state already established by sampling. assert!( requests[2] .body_json() .to_string() .contains("\"type\":\"compaction_trigger\"") ); // Phase 3: both post-compact requests keep replaying that first value. assert_eq!( requests .iter() .map(|request| request.body_json()["client_metadata"][TURN_STATE_HEADER].clone()) .collect::>(), vec![ json!(null), json!(null), json!("sampling-state"), json!("sampling-state"), json!("sampling-state"), ] ); server.shutdown().await; Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_mid_turn_continuation_compaction() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_sse_sequence( harness.server(), vec![ responses::sse(vec![ responses::ev_function_call("call-remote-mid-turn", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ]), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_MID_TURN_FINAL_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ], ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_once( harness.server(), &summary_with_prefix("REMOTE_MID_TURN_SUMMARY"), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); let requests = responses_mock.requests(); assert_eq!( requests.len(), 2, "expected initial and post-compact requests" ); let compact_request = compact_mock.single_request(); insta::assert_snapshot!( "remote_mid_turn_compaction_shapes", format_labeled_requests_snapshot( "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", &compact_request), ("Remote Post-Compaction History Layout", &requests[1]), ] ) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_mid_turn_compaction_summary_only_reinjects_context() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let initial_turn_request_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_function_call("call-remote-summary-only", DUMMY_FUNCTION_NAME, "{}"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 500), ]), ) .await; let post_compact_turn_request_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m2", "REMOTE_SUMMARY_ONLY_FINAL_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]), ) .await; let compacted_history = vec![ResponseItem::Compaction { id: None, encrypted_content: summary_with_prefix("REMOTE_SUMMARY_ONLY"), internal_chat_message_metadata_passthrough: None, }]; let compact_mock = responses::mount_compact_json_once( harness.server(), serde_json::json!({ "output": compacted_history }), ) .await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 1); assert_eq!( initial_turn_request_mock.requests().len(), 1, "expected initial turn request" ); assert_eq!( post_compact_turn_request_mock.requests().len(), 1, "expected post-compaction request" ); let compact_request = compact_mock.single_request(); let post_compact_turn_request = post_compact_turn_request_mock.single_request(); insta::assert_snapshot!( "remote_mid_turn_compaction_summary_only_reinjects_context_shapes", format_labeled_requests_snapshot( "Remote mid-turn compaction where compact output has only a compaction item: continuation layout reinjects context before that compaction item.", &[ ("Remote Compaction Request", &compact_request), ( "Remote Post-Compaction History Layout", &post_compact_turn_request ), ] ) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_pre_build_hook(allow_echo_commands) .with_config(|config| { config.model_auto_compact_token_limit = Some(200); }), ) .await?; let codex = harness.test().codex.clone(); let setup_turn_request_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("setup", "REMOTE_SETUP_REPLY"), responses::ev_completed_with_tokens("setup-response", /*total_tokens*/ 60), ]), ) .await; let second_turn_request_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_exec_command_call("call-remote-multi-summary", "echo multi-summary"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 1_000), ]), ) .await; let compact_mock = responses::mount_compact_user_history_with_summary_sequence( harness.server(), vec![ summary_with_prefix("REMOTE_OLDER_SUMMARY"), summary_with_prefix("REMOTE_LATEST_SUMMARY"), ], ) .await; codex .start_or_steer_turn(unrestricted_user_turn(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex.submit(Op::Compact).await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_TWO".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!(compact_mock.requests().len(), 2); assert_eq!( setup_turn_request_mock.requests().len(), 1, "expected setup turn request" ); assert_eq!( second_turn_request_mock.requests().len(), 1, "expected second-turn pre-compaction request" ); let compact_requests = compact_mock.requests(); assert_eq!( compact_requests.len(), 2, "expected one setup compact and one mid-turn compact request" ); let compact_request = compact_requests[1].clone(); let second_turn_request = second_turn_request_mock.single_request(); assert!( compact_request.body_contains_text("REMOTE_OLDER_SUMMARY"), "older summary should round-trip from conversation history into the next compact request" ); insta::assert_snapshot!( "remote_mid_turn_compaction_multi_summary_reinjects_above_last_summary_shapes", format_labeled_requests_snapshot( "After a prior manual /compact produced an older remote compaction item, the next turn hits remote auto-compaction before the next sampling request. The compact request carries forward that earlier compaction item, and the next sampling request shows the latest compaction item with context reinjected before USER_TWO.", &[ ("Remote Compaction Request", &compact_request), ( "Second Turn Request (After Compaction)", &second_turn_request ), ] ) ); Ok(()) } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn snapshot_request_shape_remote_manual_compact_without_previous_user_messages() -> Result<()> { skip_if_no_network!(Ok(())); let harness = TestCodexHarness::with_builder( test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()), ) .await?; let codex = harness.test().codex.clone(); let responses_mock = responses::mount_sse_once( harness.server(), responses::sse(vec![ responses::ev_assistant_message("m1", "REMOTE_MANUAL_EMPTY_FOLLOW_UP_REPLY"), responses::ev_completed_with_tokens("r1", /*total_tokens*/ 80), ]), ) .await; let compact_mock = responses::mount_compact_json_once(harness.server(), serde_json::json!({ "output": [] })) .await; codex.submit(Op::Compact).await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; codex .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { text: "USER_ONE".to_string(), text_elements: Vec::new(), }])) .await?; wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; assert_eq!( compact_mock.requests().len(), 0, "manual /compact without prior user should not issue a remote compaction request" ); let follow_up_request = responses_mock.single_request(); insta::assert_snapshot!( "remote_manual_compact_without_prev_user_shapes", format_labeled_requests_snapshot( "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", &follow_up_request)] ) ); Ok(()) }