From 3dc1e2a58406dc69db5812539adfee7d89fa9ef7 Mon Sep 17 00:00:00 2001 From: rka-oai Date: Wed, 9 Sep 2026 17:52:12 +0000 Subject: [PATCH] Always use streamed remote compaction for supported providers (#44255) ## What changed - Route manual and automatic remote compaction through the v2 implementation. - Retire the `remote_compaction_v2` feature toggle while continuing to advertise it in the model client beta features header. - Migrate compaction tests from `/responses/compact` to streamed `/responses` requests with `compaction_trigger` items and remove legacy parity coverage. ## Testing Add streamed compaction coverage for tool search output trimming and session base instruction token estimates. Update app-server tests to verify started/completed notifications for both local and streamed remote compaction. GitOrigin-RevId: cea392a6166a9b116ff83708fd9126d5546476e4 --- .../analytics/src/analytics_client_tests.rs | 4 +- .../app-server/tests/suite/v2/compaction.rs | 206 +- .../app-server/tests/suite/v2/guardian_v2.rs | 69 +- .../suite/v2/guardian_v2_history_tests.rs | 65 +- codex-rs/core/src/client.rs | 1 + codex-rs/core/src/lib.rs | 4 + codex-rs/core/src/session/mod.rs | 34 +- codex-rs/core/src/session/tests.rs | 27 +- .../core/src/session/tests/guardian_tests.rs | 43 +- codex-rs/core/src/session/turn.rs | 25 +- codex-rs/core/src/tasks/compact.rs | 12 +- codex-rs/core/tests/suite/code_mode.rs | 73 +- codex-rs/core/tests/suite/compact.rs | 378 +- codex-rs/core/tests/suite/compact_remote.rs | 3630 +---------------- .../core/tests/suite/compact_remote_parity.rs | 1216 ------ .../tests/suite/compact_remote_trimming.rs | 131 + .../core/tests/suite/cyber_access_program.rs | 45 +- codex-rs/core/tests/suite/guardian_review.rs | 23 +- codex-rs/core/tests/suite/mod.rs | 1 - .../tests/suite/reasoning_effort_override.rs | 217 +- codex-rs/core/tests/suite/responses_lite.rs | 44 +- .../tests/suite/step_settings_compaction.rs | 45 +- .../tests/suite/subagent_notifications.rs | 4 - codex-rs/core/tests/suite/token_budget.rs | 7 - codex-rs/features/src/lib.rs | 6 +- 25 files changed, 675 insertions(+), 5635 deletions(-) delete mode 100644 codex-rs/core/tests/suite/compact_remote_parity.rs create mode 100644 codex-rs/core/tests/suite/compact_remote_trimming.rs diff --git a/codex-rs/analytics/src/analytics_client_tests.rs b/codex-rs/analytics/src/analytics_client_tests.rs index fb570694c7..58ccd29382 100644 --- a/codex-rs/analytics/src/analytics_client_tests.rs +++ b/codex-rs/analytics/src/analytics_client_tests.rs @@ -1497,7 +1497,7 @@ fn compaction_event_serializes_expected_shape() { turn_id: "turn-1".to_string(), trigger: CompactionTrigger::Auto, reason: CompactionReason::ContextLimit, - implementation: CompactionImplementation::ResponsesCompact, + implementation: CompactionImplementation::ResponsesCompactionV2, phase: CompactionPhase::MidTurn, strategy: CompactionStrategy::Memento, status: CompactionStatus::Completed, @@ -1550,7 +1550,7 @@ fn compaction_event_serializes_expected_shape() { "parent_thread_id": null, "trigger": "auto", "reason": "context_limit", - "implementation": "responses_compact", + "implementation": "responses_compaction_v2", "phase": "mid_turn", "strategy": "memento", "status": "completed", diff --git a/codex-rs/app-server/tests/suite/v2/compaction.rs b/codex-rs/app-server/tests/suite/v2/compaction.rs index 8264bd68c6..9f3d1a0e61 100644 --- a/codex-rs/app-server/tests/suite/v2/compaction.rs +++ b/codex-rs/app-server/tests/suite/v2/compaction.rs @@ -1,7 +1,7 @@ //! End-to-end compaction flow tests. //! //! Phases: -//! 1) Arrange: mock responses/compact endpoints + config. +//! 1) Arrange: mock Responses endpoints + config. //! 2) Act: start a thread and submit multiple turns to trigger auto-compaction. //! 3) Assert: verify item/started + item/completed notifications for context compaction. @@ -30,14 +30,12 @@ use codex_app_server_protocol::TurnStartParams; use codex_app_server_protocol::TurnStartResponse; use codex_app_server_protocol::UserInput as V2UserInput; use codex_config::types::AuthCredentialsStoreMode; -use codex_features::Feature; -use codex_protocol::models::ContentItem; -use codex_protocol::models::ResponseItem; use codex_utils_absolute_path::test_support::PathExt; use core_test_support::responses; use core_test_support::skip_if_no_network; use pretty_assertions::assert_eq; use tempfile::TempDir; +use test_case::test_case; use tokio::time::timeout; // macOS and Windows Bazel CI can spend tens of seconds starting app-server @@ -50,8 +48,16 @@ const AUTO_COMPACT_LIMIT: i64 = 1_000; const COMPACT_PROMPT: &str = "Summarize the conversation."; const INVALID_REQUEST_ERROR_CODE: i64 = -32600; +#[derive(Clone, Copy)] +enum CompactionRoute { + Local, + Remote, +} + +#[test_case(CompactionRoute::Local; "local")] +#[test_case(CompactionRoute::Remote; "streamed remote")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()> { +async fn auto_compaction_emits_started_and_completed_items(route: CompactionRoute) -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -63,99 +69,39 @@ async fn auto_compaction_local_emits_started_and_completed_items() -> Result<()> responses::ev_assistant_message("m2", "SECOND_REPLY"), responses::ev_completed_with_tokens("r2", /*total_tokens*/ 330_000), ]); + let summary = match route { + CompactionRoute::Local => responses::ev_assistant_message("m3", "LOCAL_SUMMARY"), + CompactionRoute::Remote => serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "compaction", + "encrypted_content": "ENCRYPTED_COMPACTION_SUMMARY", + }, + }), + }; let sse3 = responses::sse(vec![ - responses::ev_assistant_message("m3", "LOCAL_SUMMARY"), + summary, responses::ev_completed_with_tokens("r3", /*total_tokens*/ 200), ]); let sse4 = responses::sse(vec![ responses::ev_assistant_message("m4", "FINAL_REPLY"), responses::ev_completed_with_tokens("r4", /*total_tokens*/ 120), ]); - responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await; + let requests = responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3, sse4]).await; let codex_home = TempDir::new()?; - compaction_config(&server.uri(), AUTO_COMPACT_LIMIT).write(codex_home.path())?; - - let mut mcp = TestAppServer::builder() - .with_codex_home(codex_home.path()) - .build_initialized_with_timeout(DEFAULT_READ_TIMEOUT) - .await?; - - let thread_id = start_thread(&mut mcp).await?; - for message in ["first", "second", "third"] { - send_turn_and_wait(&mut mcp, &thread_id, message).await?; + let mut config = compaction_config(&server.uri(), /*auto_compact_limit*/ 200_000); + if let CompactionRoute::Remote = route { + config = config + .with_provider_name("OpenAI") + .with_provider_config("requires_openai_auth = true"); + write_chatgpt_auth( + codex_home.path(), + ChatGptAuthFixture::new("access-chatgpt").plan_type("pro"), + AuthCredentialsStoreMode::File, + )?; } - - let started = wait_for_context_compaction_started(&mut mcp).await?; - let completed = wait_for_context_compaction_completed(&mut mcp).await?; - - let ThreadItem::ContextCompaction { id: started_id } = started.item else { - unreachable!("started item should be context compaction"); - }; - let ThreadItem::ContextCompaction { id: completed_id } = completed.item else { - unreachable!("completed item should be context compaction"); - }; - - assert_eq!(started.thread_id, thread_id); - assert_eq!(completed.thread_id, thread_id); - assert_eq!(started_id, completed_id); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<()> { - skip_if_no_network!(Ok(())); - const REMOTE_AUTO_COMPACT_LIMIT: i64 = 200_000; - - let server = responses::start_mock_server().await; - let sse1 = responses::sse(vec![ - responses::ev_assistant_message("m1", "FIRST_REPLY"), - responses::ev_completed_with_tokens("r1", /*total_tokens*/ 70_000), - ]); - let sse2 = responses::sse(vec![ - responses::ev_assistant_message("m2", "SECOND_REPLY"), - responses::ev_completed_with_tokens("r2", /*total_tokens*/ 330_000), - ]); - let sse3 = responses::sse(vec![ - responses::ev_assistant_message("m3", "FINAL_REPLY"), - responses::ev_completed_with_tokens("r3", /*total_tokens*/ 120), - ]); - let responses_log = responses::mount_sse_sequence(&server, vec![sse1, sse2, sse3]).await; - - let compacted_history = vec![ - ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![ContentItem::OutputText { - text: "REMOTE_COMPACT_SUMMARY".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; - - let codex_home = TempDir::new()?; - compaction_config(&server.uri(), REMOTE_AUTO_COMPACT_LIMIT) - .disable_feature(Feature::RemoteCompactionV2) - .with_provider_name("OpenAI") - .with_provider_config("requires_openai_auth = true") - .write(codex_home.path())?; - write_chatgpt_auth( - codex_home.path(), - ChatGptAuthFixture::new("access-chatgpt").plan_type("pro"), - AuthCredentialsStoreMode::File, - )?; + config.write(codex_home.path())?; let mut mcp = TestAppServer::builder() .with_codex_home(codex_home.path()) @@ -182,65 +128,31 @@ async fn auto_compaction_remote_emits_started_and_completed_items() -> Result<() assert_eq!(completed.thread_id, thread_id); assert_eq!(started_id, completed_id); - let compact_requests = compact_mock.requests(); - assert_eq!(compact_requests.len(), 1); - assert_eq!(compact_requests[0].path(), "/v1/responses/compact"); - - let response_requests = responses_log.requests(); - assert_eq!(response_requests.len(), 3); - let turn_metadata = response_requests - .iter() - .map(|request| { - request - .header("x-codex-turn-metadata") - .as_deref() - .map(parse_json_header) - .expect("turn request should include turn metadata") - }) - .collect::>(); - for (request, metadata) in response_requests.iter().zip(&turn_metadata) { - assert_eq!(metadata["request_kind"].as_str(), Some("turn")); - assert!( - metadata["turn_id"] - .as_str() - .is_some_and(|turn_id| !turn_id.is_empty()), - "turn request should carry a non-empty turn id" - ); - assert_eq!( - metadata["window_id"].as_str(), - request.header("x-codex-window-id").as_deref() - ); - assert!(metadata.get("compaction").is_none()); + let requests = requests.requests(); + assert_eq!(requests.len(), 4); + let compact_request = &requests[2]; + assert_eq!(compact_request.path(), "/v1/responses"); + match route { + CompactionRoute::Local => { + assert!( + compact_request + .inputs_of_type("compaction_trigger") + .is_empty() + ); + assert!(compact_request.body_contains_text(COMPACT_PROMPT)); + } + CompactionRoute::Remote => { + assert_eq!( + compact_request.inputs_of_type("compaction_trigger").len(), + 1 + ); + assert_eq!( + requests[3].inputs_of_type("compaction")[0]["encrypted_content"], + "ENCRYPTED_COMPACTION_SUMMARY" + ); + } } - let compact_metadata = compact_requests[0] - .header("x-codex-turn-metadata") - .as_deref() - .map(parse_json_header) - .expect("compact request should include turn metadata"); - assert_eq!( - compact_metadata["request_kind"].as_str(), - Some("compaction") - ); - assert_eq!( - compact_metadata["compaction"], - serde_json::json!({ - "trigger": "auto", - "reason": "context_limit", - "implementation": "responses_compact", - "phase": "pre_turn", - "strategy": "memento", - }) - ); - assert_eq!( - compact_metadata["turn_id"], turn_metadata[2]["turn_id"], - "pre-turn compaction should carry the current turn id" - ); - assert_eq!( - compact_metadata["window_id"].as_str(), - compact_requests[0].header("x-codex-window-id").as_deref() - ); - Ok(()) } @@ -522,10 +434,6 @@ async fn wait_for_context_compaction_completed( } } -fn parse_json_header(value: &str) -> serde_json::Value { - serde_json::from_str(value).expect("turn metadata should be JSON") -} - fn compaction_config(server_uri: &str, auto_compact_limit: i64) -> MockResponsesConfig { MockResponsesConfig::new(server_uri) .with_root_config(&format!( diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs index 0bfbe618df..ab5b6ecd31 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2.rs @@ -385,7 +385,26 @@ async fn parent_response( State(state): State>, Json(request): Json, ) -> impl IntoResponse { - let events = if request["model"] == "gpt-5.6-luna" { + let events = if request["input"].as_array().is_some_and(|input| { + input + .last() + .is_some_and(|item| item["type"] == "compaction_trigger") + }) { + assert!(state.compact_root_after_answer); + assert!(request.to_string().contains("guardian-user-input")); + vec![ + responses::ev_response_created("root-compaction"), + json!({ + "type": "response.output_item.done", + "item": { + "type": "compaction", + "id": "cmp_root", + "encrypted_content": "opaque root summary", + }, + }), + responses::ev_completed("root-compaction"), + ] + } else if request["model"] == "gpt-5.6-luna" { luna_response(&state, request).await } else if request .pointer("/client_metadata/x-openai-subagent") @@ -712,30 +731,53 @@ async fn guardian_v2_routes_scoped_tool_approvals( let responses_url = format!("http://{}", listener.local_addr()?); let router = Router::new() .route("/v1/responses", get(luna_websocket).post(parent_response)) - .route("/v1/responses/compact", post(|Json(request): Json| async move { - assert!(request.to_string().contains("guardian-user-input")); - Json(json!({"output": [{"type": "compaction", "id": "cmp_root", "encrypted_content": "opaque root summary"}]})) - })) .route( "/metrics", post( |State(state): State>, body: String| async move { if matches!(state.transcript_content, TranscriptContent::MixedEvidence) { let payload: Value = serde_json::from_str(&body).expect("OTLP JSON"); - for metric in payload["resourceMetrics"].as_array().into_iter().flatten() - .flat_map(|resource| resource["scopeMetrics"].as_array().into_iter().flatten()) + for metric in payload["resourceMetrics"] + .as_array() + .into_iter() + .flatten() + .flat_map(|resource| { + resource["scopeMetrics"].as_array().into_iter().flatten() + }) .flat_map(|scope| scope["metrics"].as_array().into_iter().flatten()) - .filter(|metric| matches!(metric["name"].as_str(), Some("codex.guardian.context.request_tokens" | "codex.guardian.context.section_cost"))) + .filter(|metric| { + matches!( + metric["name"].as_str(), + Some( + "codex.guardian.context.request_tokens" + | "codex.guardian.context.section_cost" + ) + ) + }) { let name = metric["name"].as_str().expect("context metric name"); - for point in metric["histogram"]["dataPoints"].as_array().into_iter().flatten() { - let attributes = point["attributes"].as_array().expect("metric attributes"); + for point in metric["histogram"]["dataPoints"] + .as_array() + .into_iter() + .flatten() + { + let attributes = + point["attributes"].as_array().expect("metric attributes"); for target in ["sync", "async"] { - if attributes.iter().any(|attr| attr["key"] == "target" && attr["value"]["stringValue"] == target) { - let mut bounds = state.context_metric_bounds.lock().expect("context metric bounds"); + if attributes.iter().any(|attr| { + attr["key"] == "target" + && attr["value"]["stringValue"] == target + }) { + let mut bounds = state + .context_metric_bounds + .lock() + .expect("context metric bounds"); bounds.insert( (name.to_owned(), target.to_owned()), - point["explicitBounds"].as_array().and_then(|bounds| bounds.last()).and_then(Value::as_f64), + point["explicitBounds"] + .as_array() + .and_then(|bounds| bounds.last()) + .and_then(Value::as_f64), ); if bounds.len() == 4 { state.context_metrics_recorded.notify_one(); @@ -852,7 +894,6 @@ async fn guardian_v2_routes_scoped_tool_approvals( if matches!(lifecycle, ThreadLifecycle::RootUserInputCompaction) { mock_config = mock_config .with_provider_name("OpenAI") - .disable_feature(Feature::RemoteCompactionV2) .disable_feature(Feature::TokenBudget) .disable_feature(Feature::EnableRequestCompression); } diff --git a/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs b/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs index e5b5908ff3..66be28dd7e 100644 --- a/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs +++ b/codex-rs/app-server/tests/suite/v2/guardian_v2_history_tests.rs @@ -14,7 +14,6 @@ use axum::Router; use axum::extract::State; use axum::http::header; use axum::routing::get; -use axum::routing::post; use codex_app_server_protocol::ApprovalsReviewer; use codex_app_server_protocol::AskForApproval; use codex_app_server_protocol::GuardianApprovalReview; @@ -184,12 +183,33 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac get(luna_websocket).post({ let parent_requests = Arc::clone(&parent_requests); let review_requests = Arc::clone(&review_requests); + let compact_requests = Arc::clone(&compact_requests); + let checkpoint = checkpoint.clone(); move |State(classifier): State>, Json(request): Json| { let parent_requests = Arc::clone(&parent_requests); let review_requests = Arc::clone(&review_requests); + let compact_requests = Arc::clone(&compact_requests); + let checkpoint = checkpoint.clone(); async move { - let events = if request["model"] == "gpt-5.6-luna" { + let events = if request["input"].as_array().is_some_and(|input| { + input + .iter() + .any(|item| item["type"] == "compaction_trigger") + }) { + compact_requests + .lock() + .expect("request log lock") + .push(request); + vec![ + responses::ev_assistant_message("summary", SUMMARY), + json!({ + "type": "response.output_item.done", + "item": checkpoint, + }), + responses::ev_completed("compact"), + ] + } else if request["model"] == "gpt-5.6-luna" { luna_response(&classifier, request).await } else if request["client_metadata"]["x-openai-subagent"] == "guardian" { review_requests @@ -243,29 +263,6 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac } }), ) - .route( - "/v1/responses/compact", - post({ - let compact_requests = Arc::clone(&compact_requests); - let checkpoint = checkpoint.clone(); - move |Json(request): Json| { - let compact_requests = Arc::clone(&compact_requests); - let checkpoint = checkpoint.clone(); - async move { - compact_requests - .lock() - .expect("request log lock") - .push(request); - Json(json!({"output": [ - {"type": "message", "role": "assistant", "content": [ - {"type": "output_text", "text": SUMMARY} - ]}, - checkpoint - ]})) - } - } - }), - ) .with_state(Arc::clone(&classifier)); let listener = TcpListener::bind("127.0.0.1:0").await?; let responses_url = format!("http://{}", listener.local_addr()?); @@ -288,7 +285,7 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac .enable_feature(Feature::GuardianApproval) .enable_feature(Feature::GuardianReuseParentCompaction) .disable_feature(Feature::EnableRequestCompression) - .disable_feature(Feature::RemoteCompactionV2) + .enable_feature(Feature::RemoteCompactionV2) .disable_feature(Feature::TokenBudget) .with_extra_config(&format!( "[mcp_servers.{TEST_SERVER_NAME}]\nurl = \"{mcp_url}/mcp\"\ndefault_tools_approval_mode = \"prompt\"\n\n[features.guardianv2]\nenabled = true\nthread_context = {thread_context_enabled}\npersist_scores = true\nreuse_parent_compaction = {reuse_parent_compaction}\n\n[features.guardianv2.review_scope]\ncomputer_use_only = false" @@ -616,9 +613,16 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac let output = output["output"].as_str().expect("tool output text"); assert!(output.contains(&expected_output), "{output}"); } else { - let parent_input = serde_json::to_string(&parent[index * 2 + 1]["input"])?; - assert!(!parent_input.contains(RESTRICTION)); + let parent_items = parent[index * 2 + 1]["input"] + .as_array() + .expect("request input array"); + let parent_input = serde_json::to_string(parent_items)?; + // V2 keeps bounded user history while replacing old tool output with a checkpoint. + assert_eq!(parent_input.contains(RESTRICTION), index <= 3); assert!(!parent_input.contains(EVIDENCE)); + assert!(!parent_items.iter().any(|item| { + item["type"] == "function_call_output" && item["call_id"] == "inspect-1" + })); if index <= 3 { assert!(sync_text.contains(RESTRICTION)); if matches!(context_path, ContextPath::ThreadOwned) { @@ -652,7 +656,7 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac compact_output.contains(&expected_output), "{compact_output}" ); - assert!(parent_input.contains(SUMMARY)); + assert!(parent_items.contains(&checkpoint)); if matches!(context_path, ContextPath::ThreadOwned) { assert!( content.contains(RESTRICTION), @@ -662,7 +666,8 @@ async fn guardians_retain_evidence_after_compaction_and_discard_it_after_rollbac !transcript.contains(&expected_output), "raw tool result must not survive the parent checkpoint" ); - assert!(transcript.contains(SUMMARY)); + assert!(transcript.contains(RESTRICTION)); + assert!(!transcript.contains(SUMMARY)); } else { assert!( transcript.contains(RESTRICTION), diff --git a/codex-rs/core/src/client.rs b/codex-rs/core/src/client.rs index e3dff930d7..7bb6beecbf 100644 --- a/codex-rs/core/src/client.rs +++ b/codex-rs/core/src/client.rs @@ -1278,6 +1278,7 @@ impl Drop for ModelClientSession { } impl ModelClientSession { + #[expect(dead_code, reason = "the v1-only accessor is removed in the follow-up")] pub(crate) fn turn_state(&self) -> Arc> { Arc::clone(&self.turn_state) } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 68baeb5b41..50dcbbb514 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -29,6 +29,10 @@ pub use responses_metadata::CodexResponsesMetadata; pub use turn_metadata::detached_memory_responses_metadata; mod codex_thread; mod compact_model_fallback; +#[expect( + dead_code, + reason = "the unrouted v1 runner is removed in the follow-up" +)] mod compact_remote; mod compact_remote_history; mod compact_remote_v2; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index bdd8f24c64..62f8766329 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -1088,28 +1088,20 @@ impl Session { /// /// `ModelClient` is session-scoped and intentionally does not depend on the full `Config`, so /// we precompute the comma-separated list of enabled experimental feature keys at session - /// creation time and thread it into the client. + /// creation time and thread it into the client. Remote compaction stays advertised unconditionally. fn build_model_client_beta_features_header(config: &Config) -> Option { - let beta_features_header = FEATURES - .iter() - .filter_map(|spec| { - let advertise_in_model_client_header = - spec.stage.experimental_menu_description().is_some() - || spec.id == Feature::RemoteCompactionV2; - if advertise_in_model_client_header && config.features.enabled(spec.id) { - Some(spec.key) - } else { - None - } - }) - .collect::>() - .join(","); - - if beta_features_header.is_empty() { - None - } else { - Some(beta_features_header) - } + Some( + FEATURES + .iter() + .filter(|spec| { + spec.id == Feature::RemoteCompactionV2 + || (spec.stage.experimental_menu_description().is_some() + && config.features.enabled(spec.id)) + }) + .map(|spec| spec.key) + .collect::>() + .join(","), + ) } async fn start_managed_network_proxy( diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index f50da45137..0cad00a520 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -11314,7 +11314,7 @@ async fn make_remote_compaction_session( #[test_case(FirstAttempt::Succeeds; "primary succeeds")] #[test_case(FirstAttempt::Retries; "fallback executes")] #[tokio::test] -async fn legacy_compaction_retains_only_the_selected_step(first_attempt: FirstAttempt) { +async fn remote_compaction_v2_retains_only_the_selected_step(first_attempt: FirstAttempt) { let server = responses::start_mock_server().await; let (session, turn, events) = make_remote_compaction_session(&server.uri()).await; session @@ -11360,9 +11360,15 @@ async fn legacy_compaction_retains_only_the_selected_step(first_attempt: FirstAt Some(Arc::as_ptr(&primary)), ); - let success = ResponseTemplate::new(/*status*/ 200).set_body_json(json!({ - "output": [{ "type": "compaction", "encrypted_content": "summary" }] - })); + let success = ResponseTemplate::new(/*status*/ 200) + .insert_header("content-type", "text/event-stream") + .set_body_string(responses::sse(vec![ + json!({ + "type": "response.output_item.done", + "item": { "type": "compaction", "encrypted_content": "summary" }, + }), + responses::ev_completed("compact-response"), + ])); let replies = match first_attempt { FirstAttempt::Succeeds => vec![success], FirstAttempt::Retries => vec![ @@ -11371,12 +11377,13 @@ async fn legacy_compaction_retains_only_the_selected_step(first_attempt: FirstAt success, ], }; - let requests = responses::mount_compact_response_sequence(&server, replies).await; - crate::compact_remote::run_inline_remote_auto_compact_task( + let requests = responses::mount_response_sequence(&server, replies).await; + let mut client_session = session.services.model_client.new_session(); + crate::compact_remote_v2::run_inline_remote_auto_compact_task( Arc::clone(&session), Arc::clone(&primary), Some(Arc::clone(&fallback)), - Arc::new(OnceLock::new()), + &mut client_session, InitialContextInjection::DoNotInject, CompactionReason::ModelDownshift, CompactionPhase::PreTurn, @@ -11405,6 +11412,12 @@ async fn legacy_compaction_retains_only_the_selected_step(first_attempt: FirstAt .collect::>(), models, ); + assert!( + requests + .requests() + .iter() + .all(|request| request.inputs_of_type("compaction_trigger").len() == 1), + ); session.abort_all_tasks(TurnAbortReason::Interrupted).await; recv_terminal_event(&events, TerminalEventKind::TurnAborted).await; } diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 5ca8c44a08..012da43b42 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -39,11 +39,9 @@ use codex_network_proxy::NetworkProtocol; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::error::SandboxErr; use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; -use codex_protocol::models::ContentItem; use codex_protocol::models::ContentItemKind; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::ResponseInputItem; -use codex_protocol::models::ResponseItem; use codex_protocol::protocol::AskForApproval; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -1053,7 +1051,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic } #[tokio::test] -async fn process_compacted_history_preserves_separate_guardian_developer_message() { +async fn compaction_initial_context_preserves_separate_guardian_developer_message() { let (session, mut turn_context) = make_session_and_context().await; update_turn_settings_for_test(&mut turn_context, |settings| { update_selected_settings_for_test(settings, |selected| { @@ -1084,40 +1082,20 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message step_context, }; - let (refreshed, _) = crate::compact_remote::process_compacted_history( - &session, - 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::Message { - id: None, - role: "user".to_string(), - content: vec![ContentItem::InputText { - text: "summary".to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, - ], - &initial_context_injection, - ) - .await; + let (refreshed, _) = + crate::compact::build_compaction_initial_context(&session, &initial_context_injection) + .await; let developer_messages = refreshed .iter() - .filter_map(|item| match item { + .filter_map(|envelope| match &envelope.item { ResponseItem::Message { role, content, .. } if role == "developer" => { crate::content_items_to_text(content).map(|text| { ( text, - item.executed_tool_call_metadata() + envelope + .item + .executed_tool_call_metadata() .and_then(|metadata| metadata.content_item_kinds.clone()), ) }) @@ -1126,11 +1104,6 @@ async fn process_compacted_history_preserves_separate_guardian_developer_message }) .collect::>(); - assert!( - !developer_messages - .iter() - .any(|(message, _)| message.contains("stale developer message")) - ); assert!( !developer_messages .iter() diff --git a/codex-rs/core/src/session/turn.rs b/codex-rs/core/src/session/turn.rs index 9127f3f031..c68e4a72a4 100644 --- a/codex-rs/core/src/session/turn.rs +++ b/codex-rs/core/src/session/turn.rs @@ -9,7 +9,6 @@ use crate::client_common::Prompt; use crate::client_common::ResponseEvent; use crate::compact::InitialContextInjection; use crate::compact::run_inline_auto_compact_task; -use crate::compact_remote::run_inline_remote_auto_compact_task; use crate::compact_remote_v2::run_inline_remote_auto_compact_task as run_inline_remote_auto_compact_task_v2; use crate::connectors; use crate::context::ContextualUserFragment; @@ -1315,12 +1314,7 @@ async fn run_auto_compact( } match turn_context.provider.capabilities().remote_compaction { - RemoteCompactionSupport::V2 - if turn_context - .config - .features - .enabled(Feature::RemoteCompactionV2) => - { + RemoteCompactionSupport::V2 => { emit_compact_metric( &sess.services.session_telemetry, "remote_v2", @@ -1337,23 +1331,6 @@ async fn run_auto_compact( ) .await?; } - RemoteCompactionSupport::V2 => { - emit_compact_metric( - &sess.services.session_telemetry, - "remote", - /*manual*/ false, - ); - run_inline_remote_auto_compact_task( - Arc::clone(sess), - step_context, - fallback_step_context, - client_session.turn_state(), - initial_context_injection, - reason, - phase, - ) - .await?; - } RemoteCompactionSupport::Unsupported => { emit_compact_metric( &sess.services.session_telemetry, diff --git a/codex-rs/core/src/tasks/compact.rs b/codex-rs/core/src/tasks/compact.rs index 99e2113485..14913d95c8 100644 --- a/codex-rs/core/src/tasks/compact.rs +++ b/codex-rs/core/src/tasks/compact.rs @@ -39,9 +39,7 @@ impl SessionTask for CompactTask { } let result = match ctx.provider.capabilities().remote_compaction { - RemoteCompactionSupport::V2 - if ctx.config.features.enabled(Feature::RemoteCompactionV2) => - { + RemoteCompactionSupport::V2 => { emit_compact_metric( &session.services.session_telemetry, "remote_v2", @@ -49,14 +47,6 @@ impl SessionTask for CompactTask { ); crate::compact_remote_v2::run_remote_compact_task(session.clone(), ctx).await } - RemoteCompactionSupport::V2 => { - emit_compact_metric( - &session.services.session_telemetry, - "remote", - /*manual*/ true, - ); - crate::compact_remote::run_remote_compact_task(session.clone(), ctx).await - } RemoteCompactionSupport::Unsupported => { emit_compact_metric( &session.services.session_telemetry, diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index f641fbb920..e1b0bc558f 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -1242,9 +1242,6 @@ await new Promise(() => {}); .disable(Feature::ExecutedToolCallMetadata) .expect("tool call metadata should be disabled"); } - if oversized { - let _ = config.features.disable(Feature::RemoteCompactionV2); - } }) .await?; @@ -1280,14 +1277,34 @@ await new Promise(() => {}); let cell_id = extract_running_cell_id(text_item(&first_items, /*index*/ 0)); if oversized { - responses::mount_compact_user_history_with_summary_once(&server, "compacted history") - .await; + let compact = responses::mount_sse_once( + &server, + responses::sse(vec![ + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "compaction", + "encrypted_content": "compacted history", + }, + }), + responses::ev_completed("resp-compact"), + ]), + ) + .await; test.codex.submit(Op::Compact).await?; wait_for_event(&test.codex, |event| { matches!(event, EventMsg::TurnComplete(_)) }) .await; + assert_eq!( + compact + .single_request() + .inputs_of_type("compaction_trigger") + .len(), + 1 + ); + let wait = responses::mount_function_call_agent_response( &server, "call-2", @@ -4742,20 +4759,10 @@ async fn code_mode_node_repl_text_evidence_is_visible_only_to_guardian( const OTHER_NODE_REPL_RESULT: &str = "ECHOING: guardian-visible-other-tool-result"; const UNRELATED_RESULT: &str = "ECHOING: guardian-hidden-unrelated-result"; const PRIVATE_IMAGE: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg=="; + const COMPACTION_SUMMARY: &str = "Guardian browser evidence summary"; let server = responses::start_mock_server().await; let mcp_server_bin = remote_aware_stdio_server_bin()?; let reviewer_compaction = reviewer_constraint == Some("compaction"); - let compact = if reviewer_compaction { - Some( - responses::mount_compact_user_history_with_summary_once( - &server, - "Guardian browser evidence summary", - ) - .await, - ) - } else { - None - }; let check_detail = enhanced_transcripts && transcript_images && reviewer_constraint.is_none(); let mut large_image = Cursor::new(Vec::new()); if check_detail { @@ -4798,7 +4805,6 @@ async fn code_mode_node_repl_text_evidence_is_visible_only_to_guardian( .enable(Feature::CodeMode) .expect("enable Code Mode"); if reviewer_compaction { - config.features.disable(Feature::RemoteCompactionV2).expect("use remote compaction"); config .features .enable(Feature::TokenBudget) @@ -4877,9 +4883,8 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e .replace("SNAPSHOT_PADDING", &snapshot_padding.to_string()) .replace("REVIEWER_COMPACTION", &reviewer_compaction.to_string()) .replace("LARGE_IMAGE", &check_detail.to_string()); - let response_mock = responses::mount_sse_sequence( - &server, - vec![ + let response_mock = responses::mount_sse_sequence(&server, { + let mut response_bodies = vec![ sse(vec![ responses::ev_function_call_with_namespace( "node-repl-call", @@ -4913,13 +4918,25 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e ev_completed("resp-guardian") }, ]), + ]; + if reviewer_compaction { + response_bodies.push(sse(vec![ + serde_json::json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": COMPACTION_SUMMARY}, + }), + ev_completed("resp-guardian-compact"), + ])); + } + response_bodies.extend([ sse(vec![ ev_assistant_message("guardian-again", r#"{"outcome":"allow"}"#), ev_completed("resp-guardian-again"), ]), sse(vec![ev_completed("resp-done")]), - ], - ) + ]); + response_bodies + }) .await; test.submit_text_turn(&format!("review a nested {repl_server} tool response")) .await?; @@ -4928,6 +4945,7 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e .iter() .filter(|request| { request.body_json()["client_metadata"]["x-openai-subagent"].as_str() == Some("guardian") + && request.inputs_of_type("compaction_trigger").is_empty() }) .collect::>(); assert_eq!(guardian_requests.len(), 2); @@ -5012,8 +5030,13 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e reviewer_image_urls } ); - if let Some(compact) = compact { - let compact_request = compact.single_request(); + if reviewer_compaction { + let compact_requests = requests + .iter() + .filter(|request| !request.inputs_of_type("compaction_trigger").is_empty()) + .collect::>(); + assert_eq!(compact_requests.len(), 1); + let compact_request = compact_requests[0]; assert!( compact_request .message_input_texts("user") @@ -5022,7 +5045,7 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e ); assert_eq!( guardian_requests[1].inputs_of_type("compaction")[0]["encrypted_content"], - "Guardian browser evidence summary" + COMPACTION_SUMMARY ); for request in &guardian_requests { assert!(!request.has_content_kinds(&["token_budget.context_window"])); diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index ff313b31ac..4f324ade07 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -55,8 +55,6 @@ use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_completed_with_tokens; use core_test_support::responses::ev_function_call; -use core_test_support::responses::mount_compact_json_once; -use core_test_support::responses::mount_compact_response_sequence; use core_test_support::responses::mount_response_sequence; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_once_match; @@ -1021,6 +1019,8 @@ async fn reasoning_effort_override_remote_v2_compaction_resets_pinned_effort( ev_assistant_message("m4", "third reply"), ev_completed("r4"), ]), + sse(vec![ev_completed("r5")]), + sse(vec![ev_completed("r6")]), ], ) .await; @@ -1031,10 +1031,6 @@ async fn reasoning_effort_override_remote_v2_compaction_resets_pinned_effort( model.use_responses_lite = true; }) .with_config(move |config| { - config - .features - .enable(Feature::RemoteCompactionV2) - .expect("enable remote compaction v2"); config.model_provider.stream_max_retries = Some(0); config.model_reasoning_effort = Some(ReasoningEffort::Medium); config @@ -1065,18 +1061,31 @@ async fn reasoning_effort_override_remote_v2_compaction_resets_pinned_effort( }) .await; test.submit_text_turn("after compaction").await?; + test.submit_text_turn("unchanged effort").await?; + core_test_support::submit_thread_settings( + &test.codex, + ThreadSettingsOverrides { + effort: Some(Some(ReasoningEffort::Low)), + ..Default::default() + }, + ) + .await?; + test.submit_text_turn("changed effort").await?; let requests = request_log.requests(); - assert_eq!(requests.len(), 4); + assert_eq!(requests.len(), 6); let first_body = requests[0].body_json(); let second_body = requests[1].body_json(); - let post_compaction_body = requests[3].body_json(); assert_eq!(first_body["reasoning"]["effort"], "medium"); assert_eq!(second_body["reasoning"]["effort"], "medium"); assert_eq!(requests[2].body_json()["reasoning"]["effort"], "medium"); + let post_compaction_effort = if fail_compaction { "medium" } else { "high" }; assert_eq!( - post_compaction_body["reasoning"]["effort"], - if fail_compaction { "medium" } else { "high" } + requests[3..] + .iter() + .map(|request| request.body_json()["reasoning"]["effort"].clone()) + .collect::>(), + vec![json!(post_compaction_effort); 3], ); let updates = requests .iter() @@ -1096,17 +1105,26 @@ async fn reasoning_effort_override_remote_v2_compaction_resets_pinned_effort( "type": "configuration_update", "reasoning": {"effort": "high"}, }); + let low = json!({ + "type": "configuration_update", + "reasoning": {"effort": "low"}, + }); + let retained = if fail_compaction { + vec![medium.clone(), high.clone()] + } else { + vec![] + }; + let mut changed = retained.clone(); + changed.push(low); assert_eq!( updates, [ vec![medium.clone()], vec![medium.clone(), high.clone()], - vec![medium.clone(), high.clone()], - if fail_compaction { - vec![medium, high] - } else { - vec![] - }, + vec![medium, high], + retained.clone(), + retained, + changed, ] ); @@ -2175,36 +2193,16 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { let limit = 200_000; let over_limit_tokens = 250_000; - let remote_summary = "REMOTE_COMPACT_SUMMARY"; - - let compacted_history = vec![ - codex_protocol::models::ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![codex_protocol::models::ContentItem::OutputText { - text: remote_summary.to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, - codex_protocol::models::ResponseItem::Compaction { - id: None, - encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), - internal_chat_message_metadata_passthrough: None, - }, - ]; - let compact_mock = - mount_compact_json_once(&server, serde_json::json!({ "output": compacted_history })).await; + let remote_summary = REMOTE_V2_SUMMARY; let mut builder = test_codex().with_config(move |config| { set_test_compact_prompt(config); config.model_auto_compact_token_limit = Some(limit); - let _ = config.features.disable(Feature::RemoteCompactionV2); }); let initial = builder.build(&server).await.unwrap(); // A single over-limit completion should not auto-compact until the next user message. - mount_sse_once( + let initial_response = mount_sse_once( &server, sse(vec![ ev_assistant_message("m1", FIRST_REPLY), @@ -2214,29 +2212,28 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { .await; initial.submit_turn("OVER_LIMIT_TURN").await.unwrap(); + let initial_requests = initial_response.requests(); + assert_eq!(initial_requests.len(), 1); assert!( - compact_mock.requests().is_empty(), + initial_requests[0] + .inputs_of_type("compaction_trigger") + .is_empty(), "remote compaction should not run before the next user message" ); let mut resume_builder = test_codex().with_config(move |config| { set_test_compact_prompt(config); config.model_auto_compact_token_limit = Some(limit); - let _ = config.features.disable(Feature::RemoteCompactionV2); }); let resumed = resume_builder.restart(&server, &initial).await.unwrap(); let follow_up_user = "AFTER_RESUME_USER"; + let compact_turn = remote_v2_compaction_response(); let sse_follow_up = sse(vec![ ev_assistant_message("m2", FINAL_REPLY), ev_completed("r2"), ]); - - let follow_up_matcher = move |req: &wiremock::Request| { - let body = std::str::from_utf8(&req.body).unwrap_or(""); - body.contains(follow_up_user) && body.contains(remote_summary) - }; - mount_sse_once_match(&server, follow_up_matcher, sse_follow_up).await; + let response_mock = mount_sse_sequence(&server, vec![compact_turn, sse_follow_up]).await; resumed .codex @@ -2257,17 +2254,21 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { }) .await; - let compact_requests = compact_mock.requests(); + let requests = response_mock.requests(); assert_eq!( - compact_requests.len(), - 1, - "remote compaction should run once after resume" + requests.len(), + 2, + "remote compaction should run once after resume before sampling" ); assert_eq!( - compact_requests[0].path(), - "/v1/responses/compact", - "remote compaction should hit the compact endpoint" + requests[0].path(), + "/v1/responses", + "remote compaction should use the responses endpoint" ); + assert_eq!(requests[0].inputs_of_type("compaction_trigger").len(), 1); + let follow_up_body = requests[1].body_json().to_string(); + assert!(follow_up_body.contains(follow_up_user)); + assert!(follow_up_body.contains(remote_summary)); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -2956,112 +2957,6 @@ async fn pre_sampling_compact_falls_back_after_previous_model_invalid_request_on ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn pre_sampling_legacy_remote_compact_falls_back_after_previous_model_invalid_request() { - skip_if_no_network!(); - - let server = MockServer::start().await; - let retired_model = "gpt-5.6"; - let previous_model_family = "gpt-5.6"; - let next_model = "gpt-5.5"; - let mut previous_model_info = - model_info_with_context_window("gpt-5.4", /*context_window*/ 273_000); - previous_model_info.slug = previous_model_family.to_string(); - let mut next_model_info = - model_info_with_context_window("gpt-5.4", /*context_window*/ 125_000); - next_model_info.slug = next_model.to_string(); - - let models_mock = mount_models_once( - &server, - ModelsResponse { - models: vec![previous_model_info, next_model_info], - }, - ) - .await; - let request_log = mount_sse_sequence( - &server, - vec![ - sse(vec![ - ev_assistant_message("m1", "before switch"), - ev_completed_with_tokens("r1", /*total_tokens*/ 120_000), - ]), - sse(vec![ - ev_assistant_message("m3", "after switch"), - ev_completed_with_tokens("r3", /*total_tokens*/ 100), - ]), - ], - ) - .await; - let compact_request_log = mount_compact_response_sequence( - &server, - vec![ - invalid_request_response("previous-model compaction was rejected"), - wiremock::ResponseTemplate::new(/*status*/ 200) - .insert_header("content-type", "application/json") - .set_body_json(json!({ - "output": [{ - "type": "compaction", - "encrypted_content": "DOWNSHIFT_SUMMARY", - }], - })), - ], - ) - .await; - - let model_provider = openai_model_provider(&server); - let mut builder = test_codex() - .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_model(retired_model) - .with_config(move |config| { - config.model_provider = model_provider; - set_test_compact_prompt(config); - let _ = config.features.disable(Feature::RemoteCompactionV2); - }); - let test = builder.build(&server).await.expect("build test codex"); - - test.codex - .start_or_steer_turn(disabled_permission_user_turn( - "before switch", - test.cwd.path().to_path_buf(), - retired_model.to_string(), - )) - .await - .expect("submit first user turn"); - wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) - .await; - - test.codex - .start_or_steer_turn(disabled_permission_user_turn( - "after switch", - test.cwd.path().to_path_buf(), - next_model.to_string(), - )) - .await - .expect("submit smaller-model turn"); - assert_compaction_uses_turn_lifecycle_id(&test.codex).await; - - let requests = request_log.requests(); - let compact_requests = compact_request_log.requests(); - assert_eq!(models_mock.requests().len(), 1); - assert_eq!(requests.len(), 2); - assert_eq!(compact_requests.len(), 2); - assert_eq!( - requests[0].body_json()["model"].as_str(), - Some(retired_model) - ); - assert_eq!( - compact_requests[0].body_json()["model"].as_str(), - Some(retired_model) - ); - assert_eq!( - compact_requests[1].body_json()["model"].as_str(), - Some(next_model) - ); - assert_eq!(requests[1].body_json()["model"].as_str(), Some(next_model)); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn pre_sampling_compact_keeps_unknown_previous_model_for_api_key_auth_and_custom_provider() { skip_if_no_network!(); @@ -4771,8 +4666,10 @@ async fn auto_compact_body_after_prefix_still_caps_at_context_window() { ); } +#[test_case::test_case(false; "counts_encrypted_reasoning_before_last_user")] +#[test_case::test_case(true; "compacts_after_reasoning_header_clears")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn auto_compact_counts_encrypted_reasoning_before_last_user() { +async fn auto_compact_accounts_for_encrypted_reasoning(first_response_includes_reasoning: bool) { skip_if_no_network!(); let server = start_mock_server().await; @@ -4792,51 +4689,36 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { ev_reasoning_item("post-reasoning", &["post"], &[&post_last_reasoning_content]), ev_completed_with_tokens("r2", /*total_tokens*/ 80), ]); + let compact_turn = remote_v2_compaction_response(); let third_turn = sse(vec![ ev_assistant_message("m4", FINAL_REPLY), ev_completed_with_tokens("r4", /*total_tokens*/ 1), ]); - let request_log = mount_sse_sequence( + let request_log = mount_response_sequence( &server, vec![ // Turn 1: reasoning before last user (should count). - first_turn, - // Turn 2: reasoning after last user (should be ignored for compaction). - second_turn, + if first_response_includes_reasoning { + sse_response(first_turn).insert_header("X-Reasoning-Included", "true") + } else { + sse_response(first_turn) + }, + // Turn 2: reasoning after last user should be ignored; no header is returned. + sse_response(second_turn), + // Turn 3: compact the prior conversation before sampling. + sse_response(compact_turn), // Turn 3: next user turn after remote compaction. - third_turn, + sse_response(third_turn), ], ) .await; - let compacted_history = vec![ - codex_protocol::models::ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![codex_protocol::models::ContentItem::OutputText { - text: "REMOTE_COMPACT_SUMMARY".to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, - codex_protocol::models::ResponseItem::Compaction { - id: None, - encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), - internal_chat_message_metadata_passthrough: None, - }, - ]; - let compact_mock = - mount_compact_json_once(&server, serde_json::json!({ "output": compacted_history })).await; - let chatgpt_base_url = format!("{}/backend-api", server.uri()); - let codex = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(move |config| { - config.chatgpt_base_url = chatgpt_base_url; + .with_config(|config| { set_test_compact_prompt(config); config.model_auto_compact_token_limit = Some(300); - let _ = config.features.disable(Feature::RemoteCompactionV2); }) .build(&server) .await @@ -4853,135 +4735,41 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { text_elements: Vec::new(), }])) .await - .unwrap(); + .expect("start user turn"); wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; if idx < 2 { assert!( - compact_mock.requests().is_empty(), + request_log + .requests() + .iter() + .all(|request| request.inputs_of_type("compaction_trigger").is_empty()), "remote compaction should not run before the next user turn" ); } } - let compact_requests = compact_mock.requests(); - assert_eq!( - compact_requests.len(), - 1, - "remote compaction should run once after the second turn" - ); - assert_eq!( - compact_requests[0].path(), - "/v1/responses/compact", - "remote compaction should hit the compact endpoint" - ); - let requests = request_log.requests(); assert_eq!( requests.len(), - 3, - "conversation should include three user turns" + 4, + "conversation should include three user turns and remote compaction" ); + assert_eq!(requests[2].path(), "/v1/responses"); + assert_eq!(requests[2].inputs_of_type("compaction_trigger").len(), 1); + assert!(requests[3].inputs_of_type("compaction_trigger").is_empty()); let second_request_body = requests[1].body_json().to_string(); assert!( - !second_request_body.contains("REMOTE_COMPACT_SUMMARY"), + !second_request_body.contains(REMOTE_V2_SUMMARY), "second turn should not include compacted history" ); - let third_request_body = requests[2].body_json().to_string(); + let third_request_body = requests[3].body_json().to_string(); assert!( - third_request_body.contains("REMOTE_COMPACT_SUMMARY") - || third_request_body.contains(FINAL_REPLY), - "third turn should include compacted history" - ); - assert!( - third_request_body.contains("ENCRYPTED_COMPACTION_SUMMARY"), + third_request_body.contains(REMOTE_V2_SUMMARY), "third turn should include compaction summary item" ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn auto_compact_runs_when_reasoning_header_clears_between_turns() { - skip_if_no_network!(); - - let server = start_mock_server().await; - - let first_user = "SERVER_INCLUDED_FIRST"; - let second_user = "SERVER_INCLUDED_SECOND"; - let third_user = "SERVER_INCLUDED_THIRD"; - - let pre_last_reasoning_content = "a".repeat(2_400); - let post_last_reasoning_content = "b".repeat(4_000); - - let first_turn = sse(vec![ - ev_reasoning_item("pre-reasoning", &["pre"], &[&pre_last_reasoning_content]), - ev_completed_with_tokens("r1", /*total_tokens*/ 10), - ]); - let second_turn = sse(vec![ - ev_reasoning_item("post-reasoning", &["post"], &[&post_last_reasoning_content]), - ev_completed_with_tokens("r2", /*total_tokens*/ 80), - ]); - let third_turn = sse(vec![ - ev_assistant_message("m4", FINAL_REPLY), - ev_completed_with_tokens("r4", /*total_tokens*/ 1), - ]); - - let responses = vec![ - sse_response(first_turn).insert_header("X-Reasoning-Included", "true"), - sse_response(second_turn), - sse_response(third_turn), - ]; - mount_response_sequence(&server, responses).await; - - let compacted_history = vec![ - codex_protocol::models::ResponseItem::Message { - id: None, - role: "assistant".to_string(), - content: vec![codex_protocol::models::ContentItem::OutputText { - text: "REMOTE_COMPACT_SUMMARY".to_string(), - }], - phase: None, - internal_chat_message_metadata_passthrough: None, - }, - codex_protocol::models::ResponseItem::Compaction { - id: None, - encrypted_content: "ENCRYPTED_COMPACTION_SUMMARY".to_string(), - internal_chat_message_metadata_passthrough: None, - }, - ]; - let compact_mock = - mount_compact_json_once(&server, serde_json::json!({ "output": compacted_history })).await; - - let codex = test_codex() - .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(|config| { - set_test_compact_prompt(config); - config.model_auto_compact_token_limit = Some(300); - let _ = config.features.disable(Feature::RemoteCompactionV2); - }) - .build(&server) - .await - .expect("build codex") - .codex; - - for user in [first_user, second_user, third_user] { - codex - .start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text { - text: user.into(), - text_elements: Vec::new(), - }])) - .await - .unwrap(); - wait_for_event(&codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; - } - - let compact_requests = compact_mock.requests(); - assert_eq!( - compact_requests.len(), - 1, - "remote compaction should run once after the reasoning header clears" - ); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] // TODO(ccunningham): Update once pre-turn compaction includes incoming user input. async fn snapshot_request_shape_pre_turn_compaction_including_incoming_user_message() { diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 7de3e92541..628df83957 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -1,63 +1,32 @@ -use super::compact::allow_echo_commands; -use core_test_support::test_codex::local_selections; -use std::fs; -use std::path::Path; +//! Exercises streamed remote compaction, including the retained checkpoint and continuing history. 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; @@ -66,170 +35,30 @@ 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 std::fs; +use std::path::Path; 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; +#[path = "compact_remote_trimming.rs"] +mod trimming; -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, - }] -} +const CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE: &str = + "Output exceeded the available model context and was truncated"; 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() }) } @@ -322,14 +151,6 @@ async fn close_realtime_conversation(codex: &codex_core::CodexThread) -> Result< 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, @@ -349,18 +170,6 @@ fn assert_request_contains_custom_realtime_start( ); } -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, @@ -458,76 +267,6 @@ fn assert_compact_request_omits_harness_metadata(request: &responses::ResponsesR } } -#[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(())); @@ -551,18 +290,22 @@ async fn remote_compact_v2_retains_metadata_from_resumed_history() -> Result<()> }), responses::ev_completed("response-compact"), ]), + sse(vec![responses::ev_completed("response-after")]), ], ) .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?; + let builder = || test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()); + let initial = builder() + .with_pre_build_hook(|home| { + fs::write( + home.join("config.toml"), + "[features]\nremote_compaction_v2 = false\n", + ) + .expect("write saved config"); + }) + .build(&server) + .await?; initial.submit_turn(retained_text).await?; let home = initial.home.clone(); let rollout_path = initial @@ -578,16 +321,22 @@ async fn remote_compact_v2_retains_metadata_from_resumed_history() -> Result<()> .await?; resumed.codex.submit(Op::Compact).await?; wait_for_turn_complete(&resumed.codex).await; + resumed.submit_turn("continue after compaction").await?; resumed.codex.shutdown_and_wait().await?; let requests = response_mock.requests(); - let compact_request = requests.last().context("compact request")?; + let compact_request = &requests[1]; 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)?; + assert_eq!( + requests[2].inputs_of_type("compaction")[0]["encrypted_content"], + "ANNOTATED_V2_COMPACTION_SUMMARY" + ); + assert!(requests[2].body_contains_text("continue after compaction")); Ok(()) } @@ -827,582 +576,8 @@ async fn amazon_bedrock_automatic_compaction_uses_v2_responses_endpoint() -> Res 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, @@ -1623,6 +798,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( responses::ev_completed("resp-descendant"), ]), responses::sse(vec![ + responses::ev_assistant_message("m-compact-noise", "IGNORED_COMPACT_REPLY"), serde_json::json!({ "type": "response.output_item.done", "item": { @@ -1704,7 +880,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( .await?; wait_for_turn_complete(&codex).await; - codex.submit(Op::Compact).await?; + let compact_turn_id = codex.submit(Op::Compact).await?; wait_for_turn_complete(&codex).await; codex @@ -1784,6 +960,8 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( .expect("v2 compact request should include turn metadata"), ) .expect("v2 compact turn metadata should be valid json"); + assert_eq!(compact_metadata["turn_id"], compact_turn_id); + assert_eq!(compact_metadata["root_turn_id"], compact_turn_id); assert_eq!( compact_metadata["request_kind"].as_str(), Some("compaction") @@ -1869,6 +1047,7 @@ async fn remote_compact_v2_reuses_compaction_trigger_for_followups() -> Result<( 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")); assert!( follow_up_body.contains("hello remote compact"), "expected v2 follow-up request to preserve retained original user messages" @@ -2001,425 +1180,20 @@ async fn remote_compact_v2_retries_failures_with_stream_retry_budget() -> Result Ok(()) } +#[test_case(false; "manual")] +#[test_case(true; "automatic")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_compact_v2_accepts_additional_output_items_before_compaction() -> Result<()> { +async fn remote_compact_v2_rewrites_multiple_trailing_function_call_outputs( + automatic: bool, +) -> 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 first_user_message = "turn with retained tool call"; + let second_user_message = "turn with parallel tool calls"; 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 first_trimmed_call_id = "first-trimmed-call"; + let second_trimmed_call_id = "second-trimmed-call"; + let retained_output = "retained tool output"; let harness = TestCodexHarness::with_builder( test_codex() @@ -2432,173 +1206,88 @@ async fn remote_compact_trims_function_call_history_to_fit_context_window() -> R .await?; let codex = harness.test().codex.clone(); - responses::mount_sse_sequence( + let initial_mock = mount_sse_once( 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"), - ]), - ], + sse(vec![responses::ev_completed_with_tokens( + "initial-response", + if automatic { 500_000 } else { 100 }, + )]), ) .await; + harness.test().submit_turn("initial turn").await?; + let _ = initial_mock.single_request(); + let history = [ + json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": first_user_message}]}), + json!({"type": "function_call", "call_id": retained_call_id, "name": "exec_command", "arguments": "{}"}), + json!({"type": "function_call_output", "call_id": retained_call_id, "output": retained_output}), + json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": second_user_message}]}), + json!({"type": "function_call", "call_id": first_trimmed_call_id, "name": "exec_command", "arguments": "{}"}), + json!({"type": "function_call", "call_id": second_trimmed_call_id, "name": "exec_command", "arguments": "{}"}), + json!({"type": "function_call_output", "call_id": first_trimmed_call_id, "output": "x".repeat(12_000)}), + json!({"type": "function_call_output", "call_id": second_trimmed_call_id, "output": "y".repeat(12_000)}), + ] + .into_iter() + .map(serde_json::from_value) + .collect::>>()?; + codex.inject_response_items(history).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; + let mut response_bodies = vec![sse(vec![ + json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": "REMOTE_COMPACT_SUMMARY"}, + }), + responses::ev_completed("response-compact"), + ])]; + if automatic { + response_bodies.push(responses::sse_completed("after-compact")); + } + let compact_mock = responses::mount_sse_sequence(harness.server(), response_bodies).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; + if automatic { + harness.test().submit_text_turn("after compact").await?; + } else { + codex.submit(Op::Compact).await?; + wait_for_turn_complete(&codex).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 requests = compact_mock.requests(); + assert_eq!(requests.len(), if automatic { 2 } else { 1 }); + let compact_request = &requests[0]; + assert_eq!( + compact_request.inputs_of_type("compaction_trigger").len(), + 1 + ); 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" + .any(|message| message == first_user_message) ); 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" + .any(|message| message == second_user_message) ); assert!( - compact_request.has_function_call(trimmed_call_id), - "expected compact request to retain the trailing function call" + !user_messages + .iter() + .any(|message| message == "after compact") ); + if automatic { + let followup_metadata: Value = serde_json::from_str( + &requests[1] + .header("x-codex-turn-metadata") + .context("follow-up request metadata")?, + )?; + assert_eq!(followup_metadata["request_kind"], "turn"); + } + assert!(compact_request.has_function_call(retained_call_id)); 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" + compact_request + .function_call_output_text(retained_call_id) + .as_deref(), + Some(retained_output), + "expected compact request to keep the older function output unchanged" ); assert!( compact_request.has_function_call(first_trimmed_call_id) @@ -2630,1156 +1319,9 @@ async fn remote_compact_rewrites_multiple_trailing_function_call_outputs() -> Re 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<()> { +async fn active_realtime_refreshes_changed_start_instructions_only_after_compaction() -> Result<()> +{ skip_if_no_network!(Ok(())); let server = wiremock::MockServer::start().await; @@ -3804,6 +1346,14 @@ async fn active_realtime_does_not_diff_changed_start_instructions_after_resume() vec![ responses::sse(vec![responses::ev_completed("r1")]), responses::sse(vec![responses::ev_completed("r2")]), + responses::sse(vec![ + json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": "realtime-summary"}, + }), + responses::ev_completed("r-compact"), + ]), + responses::sse(vec![responses::ev_completed("r3")]), ], ) .await; @@ -3831,9 +1381,12 @@ async fn active_realtime_does_not_diff_changed_start_instructions_after_resume() start_realtime_conversation(resumed.codex.as_ref()).await?; resumed.submit_turn("USER_TWO").await?; + resumed.codex.submit(Op::Compact).await?; + wait_for_turn_complete(&resumed.codex).await; + resumed.submit_turn("USER_THREE").await?; let requests = responses_mock.requests(); - assert_eq!(requests.len(), 2, "expected one request per session"); + assert_eq!(requests.len(), 4); assert_request_contains_custom_realtime_start(&requests[0], initial_instructions); let resumed_body = requests[1].body_json().to_string(); assert!( @@ -3844,648 +1397,19 @@ async fn active_realtime_does_not_diff_changed_start_instructions_after_resume() !resumed_body.contains(changed_instructions), "did not expect an active-to-active instruction change to emit a diff" ); + assert_eq!(requests[2].inputs_of_type("compaction_trigger").len(), 1); + assert_eq!( + requests[3].inputs_of_type("compaction")[0]["encrypted_content"], + "realtime-summary" + ); + assert_request_contains_custom_realtime_start(&requests[3], changed_instructions); + assert!(!requests[3].body_contains_text(initial_instructions)); 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(())); @@ -4666,305 +1590,3 @@ async fn remote_mid_turn_compact_v2_sends_turn_state_over_websocket() -> Result< 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(()) -} diff --git a/codex-rs/core/tests/suite/compact_remote_parity.rs b/codex-rs/core/tests/suite/compact_remote_parity.rs deleted file mode 100644 index 3c5f1606a5..0000000000 --- a/codex-rs/core/tests/suite/compact_remote_parity.rs +++ /dev/null @@ -1,1216 +0,0 @@ -use super::compact::allow_echo_commands; -use codex_core::TurnInputRequest; -use std::fs; -use std::path::Path; -use std::path::PathBuf; - -use anyhow::Result; -use codex_features::Feature; -use codex_history::RolloutItem; -use codex_login::CodexAuth; -use codex_protocol::config_types::ServiceTier; -use codex_protocol::models::PermissionProfile; -use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::EventMsg; -use codex_protocol::protocol::Op; -use codex_protocol::protocol::ThreadSettingsOverrides; -use codex_protocol::user_input::UserInput; -use core_test_support::hooks::trust_discovered_hooks; -use core_test_support::responses; -use core_test_support::responses::ResponseMock; -use core_test_support::responses::strip_response_item_ids_from_json; -use core_test_support::skip_if_no_network; -use core_test_support::test_codex::TestCodexHarness; -use core_test_support::test_codex::test_codex; -use core_test_support::wait_for_event; -use pretty_assertions::assert_eq; -use serde_json::Value; -use serde_json::json; - -const FIXED_CWD: &str = "/tmp/codex_remote_compaction_parity_workspace"; -const IMAGE_URL: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII="; -const SUMMARY: &str = "REMOTE_COMPACTION_PARITY_ENCRYPTED_SUMMARY"; -const DUMMY_FUNCTION_NAME: &str = "test_tool"; -const USER_INSTRUCTIONS: &str = "PARITY_USER_INSTRUCTIONS"; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum Mode { - Legacy, - V2, -} - -#[derive(Clone, Copy, Debug)] -enum AuthCase { - ChatGpt, - ApiKey, -} - -impl AuthCase { - fn build(self) -> CodexAuth { - match self { - AuthCase::ChatGpt => CodexAuth::create_dummy_chatgpt_auth_for_testing(), - AuthCase::ApiKey => CodexAuth::from_api_key("dummy"), - } - } -} - -#[derive(Clone, Copy, Debug)] -struct RunSettings { - auth: AuthCase, - service_tier_fast: bool, -} - -impl Default for RunSettings { - fn default() -> Self { - Self { - auth: AuthCase::ChatGpt, - service_tier_fast: false, - } - } -} - -#[derive(Clone, Copy, Debug)] -enum Step { - Assistant, - ReasoningAssistant, - FunctionTool, - ShellTool, - ImageAssistant, - WebSearchAssistant, -} - -impl Step { - fn label(self) -> &'static str { - match self { - Step::Assistant => "assistant", - Step::ReasoningAssistant => "reasoning_assistant", - Step::FunctionTool => "function_tool", - Step::ShellTool => "shell_tool", - Step::ImageAssistant => "image_assistant", - Step::WebSearchAssistant => "web_search_assistant", - } - } -} - -#[derive(Debug)] -struct Scenario { - name: &'static str, - steps: &'static [Step], -} - -#[derive(Debug)] -struct Capture { - compact_body: Value, - follow_up_body: Value, - replacement_history: Value, - normal_response_requests: usize, - compact_requests: usize, -} - -const ASSISTANT_ONLY: &[Step] = &[Step::Assistant]; -const REASONING_IMAGE: &[Step] = &[Step::ReasoningAssistant, Step::ImageAssistant]; -const TOOL_MIX: &[Step] = &[Step::Assistant, Step::FunctionTool, Step::ShellTool]; -const FULL_MIX: &[Step] = &[ - Step::ReasoningAssistant, - Step::FunctionTool, - Step::ImageAssistant, - Step::ShellTool, - Step::WebSearchAssistant, - Step::Assistant, -]; - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_compaction_parity_manual_transcripts() -> Result<()> { - skip_if_no_network!(Ok(())); - - let scenarios = [ - Scenario { - name: "assistant_only", - steps: ASSISTANT_ONLY, - }, - Scenario { - name: "reasoning_image", - steps: REASONING_IMAGE, - }, - Scenario { - name: "tool_mix", - steps: TOOL_MIX, - }, - Scenario { - name: "full_mix", - steps: FULL_MIX, - }, - ]; - - for scenario in scenarios { - compare_manual_scenario(&scenario, RunSettings::default()).await?; - } - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_compaction_parity_v2_api_key_sends_service_tier_upgrade() -> Result<()> { - skip_if_no_network!(Ok(())); - - let scenario = Scenario { - name: "api_key_service_tier", - steps: TOOL_MIX, - }; - let settings = RunSettings { - auth: AuthCase::ApiKey, - service_tier_fast: true, - }; - let legacy = run_manual_session(&scenario, Mode::Legacy, settings).await?; - let v2 = run_manual_session(&scenario, Mode::V2, settings).await?; - - assert_eq!( - legacy.compact_body.get("service_tier"), - None, - "legacy /responses/compact should continue omitting service_tier for API-key auth" - ); - assert_eq!( - v2.compact_body.get("service_tier").and_then(Value::as_str), - Some(ServiceTier::Fast.request_value()), - "v2 compaction should send service_tier through /responses for API-key auth" - ); - - assert_compact_requests_eq_except_v2_service_tier("api-key service tier", &legacy, &v2); - assert_follow_up_and_history_eq("api-key service tier", &legacy, &v2); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_compaction_parity_manual_hooks() -> Result<()> { - skip_if_no_network!(Ok(())); - - let legacy = run_manual_hook_session(Mode::Legacy).await?; - let v2 = run_manual_hook_session(Mode::V2).await?; - assert_json_eq("manual compact hook payload parity mismatch", &legacy, &v2); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_compaction_parity_pre_turn_auto() -> Result<()> { - skip_if_no_network!(Ok(())); - - let legacy = run_pre_turn_auto_session(Mode::Legacy).await?; - let v2 = run_pre_turn_auto_session(Mode::V2).await?; - assert_capture_eq("pre-turn auto", &legacy, &v2); - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn remote_compaction_parity_mid_turn_auto() -> Result<()> { - skip_if_no_network!(Ok(())); - - let legacy = run_mid_turn_auto_session(Mode::Legacy).await?; - let v2 = run_mid_turn_auto_session(Mode::V2).await?; - assert_capture_eq("mid-turn auto", &legacy, &v2); - Ok(()) -} - -async fn compare_manual_scenario(scenario: &Scenario, settings: RunSettings) -> Result<()> { - let legacy = run_manual_session(scenario, Mode::Legacy, settings).await?; - let v2 = run_manual_session(scenario, Mode::V2, settings).await?; - assert_capture_eq(scenario.name, &legacy, &v2); - Ok(()) -} - -fn assert_capture_eq(label: &str, legacy: &Capture, v2: &Capture) { - assert_eq!( - legacy.compact_requests, 1, - "legacy compact endpoint should be called exactly once for {label}", - ); - assert_eq!( - v2.compact_requests, 0, - "v2 should not call /responses/compact for {label}", - ); - - let legacy_compact = compact_request_view(&legacy.compact_body, Mode::Legacy); - let v2_compact = compact_request_view(&v2.compact_body, Mode::V2); - assert_json_eq( - &format!("compact request parity mismatch for {label}"), - &legacy_compact, - &v2_compact, - ); - - let legacy_follow_up = follow_up_request_view(&legacy.follow_up_body); - let v2_follow_up = follow_up_request_view(&v2.follow_up_body); - assert_json_eq( - &format!("post-compact follow-up request parity mismatch for {label}"), - &legacy_follow_up, - &v2_follow_up, - ); - - assert_json_eq( - &format!("replacement history parity mismatch for {label}"), - &legacy.replacement_history, - &v2.replacement_history, - ); - - println!( - "PARITY_OK scenario={} normal_response_requests={} compact_input_items={} replacement_history_items={} follow_up_input_items={}", - label, - legacy.normal_response_requests, - compact_input_len(&legacy.compact_body, Mode::Legacy), - replacement_history_len(&legacy.replacement_history), - follow_up_input_len(&legacy.follow_up_body) - ); -} - -fn assert_compact_requests_eq_except_v2_service_tier(label: &str, legacy: &Capture, v2: &Capture) { - assert_eq!( - legacy.compact_requests, 1, - "legacy compact endpoint should be called exactly once for {label}", - ); - assert_eq!( - v2.compact_requests, 0, - "v2 should not call /responses/compact for {label}", - ); - - let legacy_compact = compact_request_view(&legacy.compact_body, Mode::Legacy); - let mut v2_compact = compact_request_view(&v2.compact_body, Mode::V2); - remove_object_field(&mut v2_compact, "service_tier"); - assert_json_eq( - &format!("compact request parity mismatch for {label} after service_tier upgrade"), - &legacy_compact, - &v2_compact, - ); -} - -fn assert_follow_up_and_history_eq(label: &str, legacy: &Capture, v2: &Capture) { - let legacy_follow_up = follow_up_request_view(&legacy.follow_up_body); - let v2_follow_up = follow_up_request_view(&v2.follow_up_body); - assert_json_eq( - &format!("post-compact follow-up request parity mismatch for {label}"), - &legacy_follow_up, - &v2_follow_up, - ); - - assert_json_eq( - &format!("replacement history parity mismatch for {label}"), - &legacy.replacement_history, - &v2.replacement_history, - ); -} - -async fn run_manual_session( - scenario: &Scenario, - mode: Mode, - settings: RunSettings, -) -> Result { - let mut response_bodies = response_bodies_for_scenario(scenario); - if mode == Mode::V2 { - response_bodies.push(compaction_v2_response_body()); - } - response_bodies.push(after_compact_response_body(scenario.name)); - - let harness = build_harness(mode, settings, /*hooks*/ false).await?; - let rollout_path = rollout_path(&harness); - let codex = harness.test().codex.clone(); - - let responses_mock = responses::mount_sse_sequence(harness.server(), response_bodies).await; - let compact_mock = mount_legacy_compact_if_needed(&harness, mode).await; - - for (idx, step) in scenario.steps.iter().enumerate() { - submit_user_input(&codex, user_input_for_step(scenario.name, idx, *step)).await?; - } - - codex.submit(Op::Compact).await?; - wait_for_turn_complete(&codex).await; - - submit_user_input( - &codex, - vec![UserInput::Text { - text: format!("{}_AFTER_COMPACT_USER", scenario.name), - text_elements: Vec::new(), - }], - ) - .await?; - - capture_from_requests( - mode, - &codex, - &rollout_path, - &responses_mock, - compact_mock.as_ref(), - follow_up_index(responses_mock.requests().len()), - ) - .await -} - -async fn run_pre_turn_auto_session(mode: Mode) -> Result { - let response_bodies = match mode { - Mode::Legacy => vec![ - responses::sse(vec![ - responses::ev_assistant_message("pre-turn-first-message", "PRE_TURN_FIRST_REPLY"), - responses::ev_completed_with_tokens( - "pre-turn-first-response", - /*total_tokens*/ 500, - ), - ]), - after_compact_response_body("pre_turn_auto"), - ], - Mode::V2 => vec![ - responses::sse(vec![ - responses::ev_assistant_message("pre-turn-first-message", "PRE_TURN_FIRST_REPLY"), - responses::ev_completed_with_tokens( - "pre-turn-first-response", - /*total_tokens*/ 500, - ), - ]), - compaction_v2_response_body(), - after_compact_response_body("pre_turn_auto"), - ], - }; - let harness = build_auto_harness(mode).await?; - let rollout_path = rollout_path(&harness); - let codex = harness.test().codex.clone(); - let responses_mock = responses::mount_sse_sequence(harness.server(), response_bodies).await; - let compact_mock = mount_legacy_compact_if_needed(&harness, mode).await; - - submit_user_input( - &codex, - vec![UserInput::Text { - text: "pre_turn_auto_before".to_string(), - text_elements: Vec::new(), - }], - ) - .await?; - submit_user_input( - &codex, - vec![UserInput::Text { - text: "pre_turn_auto_after".to_string(), - text_elements: Vec::new(), - }], - ) - .await?; - - capture_from_requests( - mode, - &codex, - &rollout_path, - &responses_mock, - compact_mock.as_ref(), - follow_up_index(responses_mock.requests().len()), - ) - .await -} - -async fn run_mid_turn_auto_session(mode: Mode) -> Result { - let response_bodies = match mode { - Mode::Legacy => vec![ - responses::sse(vec![ - responses::ev_function_call("mid-turn-call", DUMMY_FUNCTION_NAME, "{}"), - responses::ev_completed_with_tokens( - "mid-turn-call-response", - /*total_tokens*/ 500, - ), - ]), - after_compact_response_body("mid_turn_auto"), - ], - Mode::V2 => vec![ - responses::sse(vec![ - responses::ev_function_call("mid-turn-call", DUMMY_FUNCTION_NAME, "{}"), - responses::ev_completed_with_tokens( - "mid-turn-call-response", - /*total_tokens*/ 500, - ), - ]), - compaction_v2_response_body(), - after_compact_response_body("mid_turn_auto"), - ], - }; - let harness = build_auto_harness(mode).await?; - let rollout_path = rollout_path(&harness); - let codex = harness.test().codex.clone(); - let responses_mock = responses::mount_sse_sequence(harness.server(), response_bodies).await; - let compact_mock = mount_legacy_compact_if_needed(&harness, mode).await; - - submit_user_input( - &codex, - vec![UserInput::Text { - text: "mid_turn_auto_user".to_string(), - text_elements: Vec::new(), - }], - ) - .await?; - - capture_from_requests( - mode, - &codex, - &rollout_path, - &responses_mock, - compact_mock.as_ref(), - follow_up_index(responses_mock.requests().len()), - ) - .await -} - -async fn run_manual_hook_session(mode: Mode) -> Result { - let response_bodies = match mode { - Mode::Legacy => vec![responses::sse(vec![ - responses::ev_assistant_message("hook-first-message", "HOOK_FIRST_REPLY"), - responses::ev_completed("hook-first-response"), - ])], - Mode::V2 => vec![ - responses::sse(vec![ - responses::ev_assistant_message("hook-first-message", "HOOK_FIRST_REPLY"), - responses::ev_completed("hook-first-response"), - ]), - compaction_v2_response_body(), - ], - }; - let harness = build_harness(mode, RunSettings::default(), /*hooks*/ true).await?; - let codex = harness.test().codex.clone(); - responses::mount_sse_sequence(harness.server(), response_bodies).await; - let compact_mock = mount_legacy_compact_if_needed(&harness, mode).await; - - submit_user_input( - &codex, - vec![UserInput::Text { - text: "manual_hooks_before".to_string(), - text_elements: Vec::new(), - }], - ) - .await?; - codex.submit(Op::Compact).await?; - wait_for_turn_complete(&codex).await; - - if let Some(compact_mock) = compact_mock { - assert_eq!(compact_mock.requests().len(), 1); - } - - let home = harness.test().codex_home_path(); - let pre = hook_log_view(&home.join("pre_compact_manual_log.jsonl"))?; - let post = hook_log_view(&home.join("post_compact_manual_log.jsonl"))?; - Ok(json!({ - "pre": pre, - "post": post, - })) -} - -async fn build_auto_harness(mode: Mode) -> Result { - build_harness_inner( - mode, - RunSettings::default(), - /*hooks*/ false, - Some(200), - ) - .await -} - -async fn build_harness(mode: Mode, settings: RunSettings, hooks: bool) -> Result { - build_harness_inner(mode, settings, hooks, /*auto_compact_limit*/ None).await -} - -async fn build_harness_inner( - mode: Mode, - settings: RunSettings, - hooks: bool, - auto_compact_limit: Option, -) -> Result { - fs::create_dir_all(FIXED_CWD)?; - let mut builder = test_codex() - .with_auth(settings.auth.build()) - .with_pre_build_hook(allow_echo_commands) - .with_pre_build_hook(|home| { - fs::write(home.join("AGENTS.md"), USER_INSTRUCTIONS) - .expect("write global instructions"); - }); - if hooks { - builder = builder.with_pre_build_hook(write_manual_compact_hooks); - } - TestCodexHarness::with_builder(builder.with_config(move |config| { - config.cwd = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(PathBuf::from( - FIXED_CWD, - )) - .expect("fixed cwd should be absolute"); - config.developer_instructions = Some("PARITY_DEVELOPER_INSTRUCTIONS".to_string()); - if settings.service_tier_fast { - config.service_tier = Some(ServiceTier::Fast.request_value().to_string()); - } - config.model_auto_compact_token_limit = auto_compact_limit; - if hooks { - trust_discovered_hooks(config); - } - if mode == Mode::Legacy { - let _ = config.features.disable(Feature::RemoteCompactionV2); - } - })) - .await -} - -fn rollout_path(harness: &TestCodexHarness) -> PathBuf { - harness - .test() - .session_configured - .rollout_path - .clone() - .expect("rollout path") -} - -async fn mount_legacy_compact_if_needed( - harness: &TestCodexHarness, - mode: Mode, -) -> Option { - match mode { - Mode::Legacy => Some( - responses::mount_compact_user_history_with_summary_once(harness.server(), SUMMARY) - .await, - ), - Mode::V2 => None, - } -} - -fn follow_up_index(request_count: usize) -> usize { - request_count.checked_sub(1).expect("follow-up request") -} - -async fn capture_from_requests( - mode: Mode, - codex: &codex_core::CodexThread, - rollout_path: &Path, - responses_mock: &ResponseMock, - compact_mock: Option<&ResponseMock>, - follow_up_index: usize, -) -> Result { - let response_requests = responses_mock.requests(); - let follow_up_body = response_requests - .get(follow_up_index) - .expect("follow-up request should be present") - .body_json(); - - let (compact_body, compact_requests) = match (mode, compact_mock) { - (Mode::Legacy, Some(compact_mock)) => { - let compact_requests = compact_mock.requests().len(); - (compact_mock.single_request().body_json(), compact_requests) - } - (Mode::V2, None) => { - let compact_index = follow_up_index - .checked_sub(1) - .expect("v2 compact request should precede follow-up"); - (response_requests[compact_index].body_json(), 0) - } - (Mode::Legacy, None) | (Mode::V2, Some(_)) => panic!("unexpected compact mock state"), - }; - - codex.submit(Op::Shutdown).await?; - wait_for_event(codex, |ev| matches!(ev, EventMsg::ShutdownComplete)).await; - - Ok(Capture { - compact_body, - follow_up_body, - replacement_history: replacement_history_from_rollout(rollout_path)?, - normal_response_requests: response_requests.len(), - compact_requests, - }) -} - -async fn submit_user_input(codex: &codex_core::CodexThread, items: Vec) -> Result<()> { - codex - .start_or_steer_turn(TurnInputRequest::user_input(items).with_thread_settings( - ThreadSettingsOverrides { - approval_policy: Some(AskForApproval::Never), - permission_profile: Some(PermissionProfile::Disabled), - ..Default::default() - }, - )) - .await?; - wait_for_turn_complete(codex).await; - Ok(()) -} - -async fn wait_for_turn_complete(codex: &codex_core::CodexThread) { - wait_for_event(codex, |ev| matches!(ev, EventMsg::TurnComplete(_))).await; -} - -fn user_input_for_step(scenario_name: &str, idx: usize, step: Step) -> Vec { - let mut items = Vec::new(); - if matches!(step, Step::ImageAssistant) { - items.push(UserInput::Image { - image_url: IMAGE_URL.to_string(), - detail: None, - }); - } - items.push(UserInput::Text { - text: format!("{}_USER_TURN_{}_{}", scenario_name, idx, step.label()), - text_elements: Vec::new(), - }); - items -} - -fn response_bodies_for_scenario(scenario: &Scenario) -> Vec { - scenario - .steps - .iter() - .enumerate() - .flat_map(|(idx, step)| response_bodies_for_step(scenario.name, idx, *step)) - .collect() -} - -fn response_bodies_for_step(scenario_name: &str, idx: usize, step: Step) -> Vec { - let response_id = format!("{scenario_name}-{idx}-{}", step.label()); - match step { - Step::Assistant => vec![responses::sse(vec![ - responses::ev_assistant_message( - &format!("{response_id}-message"), - &format!("{scenario_name} assistant reply {idx}"), - ), - responses::ev_completed(&format!("{response_id}-response")), - ])], - Step::ReasoningAssistant => vec![responses::sse(vec![ - responses::ev_reasoning_item( - &format!("{response_id}-reasoning"), - &["PARITY_REASONING_SUMMARY"], - &["parity raw reasoning content"], - ), - responses::ev_assistant_message( - &format!("{response_id}-message"), - &format!("{scenario_name} reasoning reply {idx}"), - ), - responses::ev_completed(&format!("{response_id}-response")), - ])], - Step::FunctionTool => vec![ - responses::sse(vec![ - responses::ev_function_call( - &format!("{response_id}-call"), - DUMMY_FUNCTION_NAME, - r#"{"case":"parity"}"#, - ), - responses::ev_completed(&format!("{response_id}-tool-response")), - ]), - responses::sse(vec![ - responses::ev_assistant_message( - &format!("{response_id}-final-message"), - &format!("{scenario_name} function follow-up {idx}"), - ), - responses::ev_completed(&format!("{response_id}-final-response")), - ]), - ], - Step::ShellTool => vec![ - responses::sse(vec![ - responses::ev_exec_command_call( - &format!("{response_id}-shell-call"), - &format!("echo {scenario_name}_{idx}_SHELL_TOOL"), - ), - responses::ev_completed(&format!("{response_id}-shell-response")), - ]), - responses::sse(vec![ - responses::ev_assistant_message( - &format!("{response_id}-final-message"), - &format!("{scenario_name} shell follow-up {idx}"), - ), - responses::ev_completed(&format!("{response_id}-final-response")), - ]), - ], - Step::ImageAssistant => vec![responses::sse(vec![ - responses::ev_assistant_message( - &format!("{response_id}-message"), - &format!("{scenario_name} image reply {idx}"), - ), - responses::ev_completed(&format!("{response_id}-response")), - ])], - Step::WebSearchAssistant => vec![responses::sse(vec![ - responses::ev_web_search_call_done( - &format!("{response_id}-web-search"), - "completed", - &format!("{scenario_name} parity query"), - ), - responses::ev_assistant_message( - &format!("{response_id}-message"), - &format!("{scenario_name} web search reply {idx}"), - ), - responses::ev_completed(&format!("{response_id}-response")), - ])], - } -} - -fn compaction_v2_response_body() -> String { - responses::sse(vec![ - json!({ - "type": "response.output_item.done", - "item": { - "type": "compaction", - "encrypted_content": SUMMARY, - } - }), - responses::ev_completed("remote-compaction-v2-response"), - ]) -} - -fn after_compact_response_body(scenario_name: &str) -> String { - responses::sse(vec![ - responses::ev_assistant_message( - &format!("{scenario_name}-after-compact-message"), - &format!("{scenario_name} after compact reply"), - ), - responses::ev_completed(&format!("{scenario_name}-after-compact-response")), - ]) -} - -fn compact_request_view(body: &Value, mode: Mode) -> Value { - let mut input = body - .get("input") - .and_then(Value::as_array) - .cloned() - .expect("compact request should include input"); - if mode == Mode::V2 { - let trigger = input - .pop() - .expect("v2 compact input should end with trigger"); - assert_eq!( - trigger, - json!({"type": "compaction_trigger"}), - "v2 compact input should append exactly one compaction_trigger" - ); - } - - let mut selected = selected_request_fields(body, SelectedFieldsMode::Compact); - selected["input"] = normalize_value(strip_response_item_ids_from_json(Value::Array(input))); - canonical_json(&normalize_value(selected)) -} - -fn follow_up_request_view(body: &Value) -> Value { - let mut selected = selected_request_fields(body, SelectedFieldsMode::FollowUp); - selected["input"] = normalize_value(strip_response_item_ids_from_json( - body.get("input") - .cloned() - .expect("follow-up request should include input"), - )); - canonical_json(&normalize_value(selected)) -} - -fn replacement_history_from_rollout(path: &Path) -> Result { - let rollout_text = fs::read_to_string(path)?; - let mut replacement_history = None; - for line in rollout_text - .lines() - .map(str::trim) - .filter(|line| !line.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(items) = compacted.replacement_history - { - let values = items - .into_iter() - .map(|item| serde_json::to_value(item.item).expect("serialize replacement item")) - .collect::>(); - replacement_history = Some(Value::Array(values)); - } - } - let replacement_history = - replacement_history.expect("expected compacted rollout replacement history"); - Ok(canonical_json(&normalize_value( - strip_response_item_ids_from_json(replacement_history), - ))) -} - -fn write_manual_compact_hooks(home: &Path) { - write_hook_script( - &home.join("pre_compact_manual.py"), - &home.join("pre_compact_manual_log.jsonl"), - ); - write_hook_script( - &home.join("post_compact_manual.py"), - &home.join("post_compact_manual_log.jsonl"), - ); - let hooks = json!({ - "hooks": { - "PreCompact": [{ - "matcher": "manual", - "hooks": [{ - "type": "command", - "command": python_hook_command(&home.join("pre_compact_manual.py")), - }] - }], - "PostCompact": [{ - "matcher": "manual", - "hooks": [{ - "type": "command", - "command": python_hook_command(&home.join("post_compact_manual.py")), - }] - }] - } - }); - fs::write(home.join("hooks.json"), hooks.to_string()).expect("write hooks.json"); -} - -fn write_hook_script(script_path: &Path, log_path: &Path) { - let script = format!( - r#"import json -from pathlib import Path -import sys - -payload = json.load(sys.stdin) -with Path(r"{log_path}").open("a", encoding="utf-8") as handle: - handle.write(json.dumps(payload, sort_keys=True) + "\n") -"#, - log_path = log_path.display(), - ); - fs::write(script_path, script).expect("write compact hook script"); -} - -fn python_hook_command(script_path: &Path) -> String { - format!("python3 \"{}\"", script_path.display()) -} - -fn hook_log_view(path: &Path) -> Result { - let text = fs::read_to_string(path)?; - let values = text - .lines() - .filter(|line| !line.trim().is_empty()) - .map(|line| { - let payload: Value = serde_json::from_str(line).expect("parse compact hook payload"); - json!({ - "hook_event_name": payload["hook_event_name"].clone(), - "trigger": payload["trigger"].clone(), - "model": payload["model"].clone(), - "has_reason": payload.get("reason").is_some(), - "has_phase": payload.get("phase").is_some(), - "has_implementation": payload.get("implementation").is_some(), - "has_status": payload.get("status").is_some(), - "has_error": payload.get("error").is_some(), - }) - }) - .collect::>(); - Ok(Value::Array(values)) -} - -#[derive(Clone, Copy)] -enum SelectedFieldsMode { - Compact, - FollowUp, -} - -fn selected_request_fields(body: &Value, mode: SelectedFieldsMode) -> Value { - let mut selected = serde_json::Map::new(); - let fields: &[&str] = match mode { - SelectedFieldsMode::Compact => &[ - "model", - "instructions", - "parallel_tool_calls", - "reasoning", - "service_tier", - "prompt_cache_key", - "text", - "tools", - "previous_response_id", - ], - SelectedFieldsMode::FollowUp => &[ - "model", - "instructions", - "parallel_tool_calls", - "reasoning", - "service_tier", - "prompt_cache_key", - "text", - "tools", - "tool_choice", - "previous_response_id", - "store", - "stream", - "include", - ], - }; - for field in fields { - if let Some(value) = body.get(field) { - selected.insert((*field).to_string(), normalize_value(value.clone())); - } - } - Value::Object(selected) -} - -fn normalize_value(value: Value) -> Value { - match value { - Value::String(text) => Value::String(normalize_string(&text)), - Value::Array(values) => Value::Array(values.into_iter().map(normalize_value).collect()), - Value::Object(map) => Value::Object( - map.into_iter() - .filter(|(key, _value)| key != "internal_chat_message_metadata_passthrough") - .map(|(key, value)| (key, normalize_value(value))) - .collect(), - ), - Value::Null | Value::Bool(_) | Value::Number(_) => value, - } -} - -fn normalize_string(value: &str) -> String { - if is_uuid_like(value) { - return "".to_string(); - } - - let mut text = value.to_string(); - normalize_tmp_prefix_before_marker(&mut text, "/skills/"); - normalize_tmp_prefix_before_marker(&mut text, "\\skills\\"); - - let mut search_start = 0; - let chunk_id_prefix = "Chunk ID: "; - while let Some(relative_start) = text[search_start..].find(chunk_id_prefix) { - let value_start = search_start + relative_start + chunk_id_prefix.len(); - let value_end = text[value_start..] - .find('\n') - .map_or(text.len(), |offset| value_start + offset); - let value = &text[value_start..value_end]; - if !value.is_empty() && value.chars().all(|ch| ch.is_ascii_hexdigit()) { - text.replace_range(value_start..value_end, ""); - search_start = value_start + "".len(); - } else { - search_start = value_end; - } - } - - let skills_open_tag = ""; - let skills_close_tag = ""; - let mut search_start = 0; - while let Some(relative_start) = text[search_start..].find(skills_open_tag) { - let body_start = search_start + relative_start + skills_open_tag.len(); - let Some(relative_end) = text[body_start..].find(skills_close_tag) else { - break; - }; - let body_end = body_start + relative_end; - text.replace_range(body_start..body_end, "\n...\n"); - search_start = body_start + "\n...\n".len() + skills_close_tag.len(); - } - - let mut search_start = 0; - let wall_time_prefix = "Wall time: "; - let wall_time_suffix = " seconds"; - while let Some(relative_start) = text[search_start..].find(wall_time_prefix) { - let value_start = search_start + relative_start + wall_time_prefix.len(); - let Some(relative_end) = text[value_start..].find(wall_time_suffix) else { - break; - }; - let value_end = value_start + relative_end; - let value = &text[value_start..value_end]; - if !value.is_empty() - && value.chars().any(|ch| ch.is_ascii_digit()) - && value.chars().all(|ch| ch.is_ascii_digit() || ch == '.') - { - text.replace_range(value_start..value_end, ""); - search_start = value_start + "".len() + wall_time_suffix.len(); - } else { - search_start = value_end + wall_time_suffix.len(); - } - } - text -} - -#[test] -fn normalize_string_rewrites_dynamic_skill_instructions() { - let text = normalize_string( - "before\n\n## Skills\n- demo: Dynamic description\n\ - \nafter", - ); - - assert_eq!( - text, - "before\n\n...\n\nafter" - ); -} - -fn is_uuid_like(value: &str) -> bool { - let bytes = value.as_bytes(); - bytes.len() == 36 - && [8, 13, 18, 23].iter().all(|idx| bytes[*idx] == b'-') - && bytes - .iter() - .enumerate() - .all(|(idx, byte)| [8, 13, 18, 23].contains(&idx) || byte.is_ascii_hexdigit()) -} - -fn normalize_tmp_prefix_before_marker(text: &mut String, marker: &str) { - let mut search_start = 0; - while let Some(relative_marker_index) = text[search_start..].find(marker) { - let marker_index = search_start + relative_marker_index; - let prefix = &text[..marker_index]; - let windows_appdata_temp_start = prefix - .rfind("/AppData/Local/Temp/.tmp") - .and_then(|temp_index| prefix[..temp_index].rfind(":/Users/")) - .and_then(|colon_index| colon_index.checked_sub(1)) - .or_else(|| { - prefix - .rfind("\\AppData\\Local\\Temp\\.tmp") - .and_then(|temp_index| prefix[..temp_index].rfind(":\\Users\\")) - .and_then(|colon_index| colon_index.checked_sub(1)) - }); - let start = prefix - .rfind("/private/var/folders/") - .or_else(|| prefix.rfind("/var/folders/")) - .or_else(|| prefix.rfind("/private/tmp/.tmp")) - .or_else(|| prefix.rfind("/tmp/.tmp")) - .or(windows_appdata_temp_start); - if let Some(start_index) = start { - text.replace_range(start_index..marker_index, ""); - search_start = start_index + "".len() + marker.len(); - } else { - search_start = marker_index + marker.len(); - } - } -} - -#[test] -fn normalize_string_rewrites_linux_temp_skill_paths() { - let text = normalize_string( - "file: /tmp/.tmp5YYdK3/skills/.system/imagegen/SKILL.md and \ - /private/tmp/.tmpw3wqF9/skills/custom/SKILL.md", - ); - - assert_eq!( - text, - "file: /skills/.system/imagegen/SKILL.md and \ - /skills/custom/SKILL.md" - ); -} - -#[test] -fn normalize_string_rewrites_windows_temp_skill_paths() { - let text = normalize_string( - "file: C:/Users/runneradmin/AppData/Local/Temp/.tmpDuYxa3/skills/.system/imagegen/SKILL.md and \ - C:\\Users\\runneradmin\\AppData\\Local\\Temp\\.tmpiP36Yr\\skills\\custom\\SKILL.md", - ); - - assert_eq!( - text, - "file: /skills/.system/imagegen/SKILL.md and \ - \\skills\\custom\\SKILL.md" - ); -} - -#[test] -fn normalize_string_rewrites_shell_wall_times() { - let text = normalize_string( - "Exit code: 0\nWall time: 0 seconds\nOutput:\nok\n\ - Exit code: 0\nWall time: 0.1 seconds\nOutput:\nok", - ); - - assert_eq!( - text, - "Exit code: 0\nWall time: seconds\nOutput:\nok\n\ - Exit code: 0\nWall time: seconds\nOutput:\nok" - ); -} - -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(), - } -} - -fn remove_object_field(value: &mut Value, field: &str) { - if let Value::Object(map) = value { - map.remove(field); - } -} - -fn assert_json_eq(label: &str, left: &Value, right: &Value) { - if left != right { - panic!("{}\n{}", label, first_json_diff(left, right, "$")); - } -} - -fn first_json_diff(left: &Value, right: &Value, path: &str) -> String { - match (left, right) { - (Value::Object(left_map), Value::Object(right_map)) => { - let mut keys = left_map.keys().chain(right_map.keys()).collect::>(); - keys.sort(); - keys.dedup(); - for key in keys { - match (left_map.get(key), right_map.get(key)) { - (Some(left_value), Some(right_value)) if left_value != right_value => { - return first_json_diff(left_value, right_value, &format!("{path}.{key}")); - } - (None, Some(right_value)) => { - return format!( - "{path}.{key}: missing on left, right={}", - short_json(right_value) - ); - } - (Some(left_value), None) => { - return format!( - "{path}.{key}: left={}, missing on right", - short_json(left_value) - ); - } - (Some(_), Some(_)) | (None, None) => {} - } - } - format!("{path}: object mismatch") - } - (Value::Array(left_values), Value::Array(right_values)) => { - let len = left_values.len().min(right_values.len()); - for idx in 0..len { - if left_values[idx] != right_values[idx] { - return first_json_diff( - &left_values[idx], - &right_values[idx], - &format!("{path}[{idx}]"), - ); - } - } - if left_values.len() != right_values.len() { - return format!( - "{path}: array len left={} right={}", - left_values.len(), - right_values.len() - ); - } - format!("{path}: array mismatch") - } - _ => format!( - "{path}: left={} right={}", - short_json(left), - short_json(right) - ), - } -} - -fn short_json(value: &Value) -> String { - let text = serde_json::to_string(value).expect("serialize short json value"); - const MAX_LEN: usize = 1000; - if text.len() <= MAX_LEN { - text - } else { - let prefix = text.chars().take(MAX_LEN).collect::(); - format!("{prefix}...<{} chars>", text.len()) - } -} - -fn compact_input_len(body: &Value, mode: Mode) -> usize { - let len = body - .get("input") - .and_then(Value::as_array) - .map(Vec::len) - .unwrap_or_default(); - match mode { - Mode::Legacy => len, - Mode::V2 => len.saturating_sub(1), - } -} - -fn follow_up_input_len(body: &Value) -> usize { - body.get("input") - .and_then(Value::as_array) - .map(Vec::len) - .unwrap_or_default() -} - -fn replacement_history_len(body: &Value) -> usize { - body.as_array().map(Vec::len).unwrap_or_default() -} diff --git a/codex-rs/core/tests/suite/compact_remote_trimming.rs b/codex-rs/core/tests/suite/compact_remote_trimming.rs new file mode 100644 index 0000000000..04ed0f552a --- /dev/null +++ b/codex-rs/core/tests/suite/compact_remote_trimming.rs @@ -0,0 +1,131 @@ +//! Checks request-history trimming at the streamed remote compaction boundary. + +use super::*; +use core_test_support::apps_test_server::configure_search_capable_model; +use pretty_assertions::assert_eq; + +fn compact_response() -> String { + sse(vec![ + json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": "TRIMMING_SUMMARY"}, + }), + responses::ev_completed("compact"), + ]) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_compact_v2_trims_tool_search_output_to_empty_tools_array() -> Result<()> { + skip_if_no_network!(Ok(())); + + let call_id = "tool-search-1"; + let tools = json!([{ + "type": "namespace", "name": "codex_app", "description": "Codex app tools.", + "tools": [{ + "type": "function", "name": "oversized_dynamic_tool", "description": "x".repeat(20_000), + "parameters": {"type": "object", "properties": {}, "additionalProperties": false}, + "strict": false, "defer_loading": true, + }], + }]); + for (context_window, expected_tools) in [(200_000, &tools), (2_000, &json!([]))] { + let harness = TestCodexHarness::with_builder( + test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config(move |config| { + configure_search_capable_model(config); + config.model_context_window = Some(context_window); + }), + ) + .await?; + let codex = &harness.test().codex; + let history = [ + json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": "find the tool"}]}), + json!({"type": "tool_search_call", "call_id": call_id, "execution": "client", "arguments": {"query": "oversized deferred tool"}}), + json!({"type": "tool_search_output", "call_id": call_id, "status": "completed", "execution": "client", "tools": &tools}), + ] + .into_iter() + .map(serde_json::from_value) + .collect::>>()?; + codex.inject_response_items(history).await?; + let mock = mount_sse_once(harness.server(), compact_response()).await; + codex.submit(Op::Compact).await?; + wait_for_turn_complete(codex).await; + + let request = mock.single_request(); + assert_eq!(request.path(), "/v1/responses"); + assert_eq!(request.inputs_of_type("compaction_trigger").len(), 1); + assert!( + request + .inputs_of_type("tool_search_call") + .iter() + .any(|item| item["call_id"] == call_id) + ); + let output = request.tool_search_output(call_id); + assert_eq!(&output["tools"], expected_tools); + assert_eq!(output["call_id"], call_id); + assert_eq!(output["status"], "completed"); + assert_eq!(output["execution"], "client"); + } + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn remote_compact_v2_trim_estimate_uses_session_base_instructions() -> Result<()> { + skip_if_no_network!(Ok(())); + + let short_instructions = "session base instructions"; + let long_instructions = format!("{short_instructions} {}", "x".repeat(24_000)); + let trailing_output = "x".repeat(12_000); + for (instructions, expected_output) in [ + (short_instructions, trailing_output.as_str()), + ( + long_instructions.as_str(), + CONTEXT_WINDOW_TRUNCATED_OUTPUT_MESSAGE, + ), + ] { + let harness = TestCodexHarness::with_builder( + test_codex() + .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) + .with_config({ + let instructions = instructions.to_string(); + move |config| { + config.model_context_window = Some(8_000); + config.base_instructions = Some(instructions); + } + }), + ) + .await?; + let codex = &harness.test().codex; + let history = [ + json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": "older user"}]}), + json!({"type": "function_call", "call_id": "retained", "name": "exec_command", "arguments": "{}"}), + json!({"type": "function_call_output", "call_id": "retained", "output": "retained output"}), + json!({"type": "message", "role": "user", "content": [{"type": "input_text", "text": "user boundary"}]}), + json!({"type": "function_call", "call_id": "trailing", "name": "exec_command", "arguments": "{}"}), + json!({"type": "function_call_output", "call_id": "trailing", "output": &trailing_output}), + ] + .into_iter() + .map(serde_json::from_value) + .collect::>>()?; + codex.inject_response_items(history).await?; + let mock = mount_sse_once(harness.server(), compact_response()).await; + codex.submit(Op::Compact).await?; + wait_for_turn_complete(codex).await; + + let request = mock.single_request(); + assert_eq!(request.path(), "/v1/responses"); + assert_eq!(request.inputs_of_type("compaction_trigger").len(), 1); + assert_eq!(request.instructions_text(), instructions); + assert!(request.has_function_call("retained")); + assert!(request.has_function_call("trailing")); + assert_eq!( + request.function_call_output_text("retained").as_deref(), + Some("retained output") + ); + assert_eq!( + request.function_call_output_text("trailing").as_deref(), + Some(expected_output) + ); + } + Ok(()) +} diff --git a/codex-rs/core/tests/suite/cyber_access_program.rs b/codex-rs/core/tests/suite/cyber_access_program.rs index 8c12c95e9b..6797f80655 100644 --- a/codex-rs/core/tests/suite/cyber_access_program.rs +++ b/codex-rs/core/tests/suite/cyber_access_program.rs @@ -188,49 +188,6 @@ async fn cyber_access_program_omits_api_key_and_spoofed_custom_provider() -> Res Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn cyber_access_program_survives_mid_turn_remote_compaction() -> Result<()> { - core_test_support::skip_if_no_network!(Ok(())); - let server = responses::start_mock_server().await; - let requests = responses::mount_sse_sequence( - &server, - vec![ - responses::sse(vec![ - // A dummy tool response forces a follow-up without shell approvals. - responses::ev_function_call("tool-1", "test_tool", "{}"), - responses::ev_completed_with_tokens("resp-1", /*total_tokens*/ 100000000), - ]), - responses::sse(vec![responses::ev_completed("resp-2")]), - ], - ) - .await; - let compact = - responses::mount_compact_user_history_with_summary_once(&server, "compacted history").await; - let test = test_codex() - .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(|config| { - let _ = config.features.disable(Feature::RemoteCompactionV2); - }) - .build_with_auto_env(&server) - .await?; - - submit(&test, Some(CyberAccessProgram::DaybreakBlue)).await?; - - assert_eq!( - compact.single_request().body_json()["access_programs"], - json!({"cyber": "daybreak_blue"}) - ); - assert_eq!( - requests - .requests() - .iter() - .map(|request| request.body_json()["access_programs"].clone()) - .collect::>(), - vec![json!({"cyber": "daybreak_blue"}); 2] - ); - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn cyber_access_program_survives_mid_turn_remote_compaction_v2() -> Result<()> { core_test_support::skip_if_no_network!(Ok(())); @@ -261,7 +218,7 @@ async fn cyber_access_program_survives_mid_turn_remote_compaction_v2() -> Result let test = test_codex() .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) .with_config(|config| { - let _ = config.features.enable(Feature::RemoteCompactionV2); + let _ = config.features.disable(Feature::RemoteCompactionV2); config.model_auto_compact_token_limit = Some(200); }) .build_with_auto_env(&server) diff --git a/codex-rs/core/tests/suite/guardian_review.rs b/codex-rs/core/tests/suite/guardian_review.rs index cad3d7ce11..0593841618 100644 --- a/codex-rs/core/tests/suite/guardian_review.rs +++ b/codex-rs/core/tests/suite/guardian_review.rs @@ -302,10 +302,6 @@ async fn guardian_review_compacts_with_summary_despite_parent_token_budget( let server = start_mock_server().await; let summary = "Guardian retained the user's standing authorization."; - let compact = core_test_support::responses::mount_compact_user_history_with_summary_once( - &server, summary, - ) - .await; let mut builder = test_codex() .with_model_info_override("gpt-5.5", |model| { model.auto_review_model_override = Some(model.slug.clone()); @@ -328,10 +324,6 @@ async fn guardian_review_compacts_with_summary_despite_parent_token_budget( .features .set_enabled(Feature::GuardianThreadContext, thread_owned) .expect("configure Guardian context mode"); - config - .features - .disable(Feature::RemoteCompactionV2) - .expect("use remote compaction"); config.model_context_window = Some(100_000); config.model_auto_compact_token_limit = Some(50_000); config.permissions.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); @@ -367,6 +359,13 @@ async fn guardian_review_compacts_with_summary_despite_parent_token_budget( ev_function_call("exec-second", "exec_command", &command), ev_completed("resp-parent-second"), ]), + sse(vec![ + json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": summary}, + }), + ev_completed("resp-guardian-compact"), + ]), sse(vec![ ev_response_created("resp-guardian-second"), ev_assistant_message("guardian-second", approval), @@ -389,6 +388,7 @@ async fn guardian_review_compacts_with_summary_despite_parent_token_budget( .iter() .filter(|request| { request.body_json()["client_metadata"]["x-openai-subagent"].as_str() == Some("guardian") + && request.inputs_of_type("compaction_trigger").is_empty() }) .collect::>(); assert_eq!(guardian_requests.len(), 2); @@ -402,7 +402,12 @@ async fn guardian_review_compacts_with_summary_despite_parent_token_budget( for request in &guardian_requests { assert!(!request.has_content_kinds(&["token_budget.context_window"])); } - let compact_request = compact.single_request(); + let compact_requests = requests + .iter() + .filter(|request| !request.inputs_of_type("compaction_trigger").is_empty()) + .collect::>(); + assert_eq!(compact_requests.len(), 1); + let compact_request = compact_requests[0]; assert!( compact_request .message_input_texts("user") diff --git a/codex-rs/core/tests/suite/mod.rs b/codex-rs/core/tests/suite/mod.rs index 73c08c5452..0791c5be2e 100644 --- a/codex-rs/core/tests/suite/mod.rs +++ b/codex-rs/core/tests/suite/mod.rs @@ -56,7 +56,6 @@ mod codex_delegate; mod collaboration_instructions; mod compact; mod compact_remote; -mod compact_remote_parity; mod compact_resume_fork; mod context_annotations; mod current_time_reminder; diff --git a/codex-rs/core/tests/suite/reasoning_effort_override.rs b/codex-rs/core/tests/suite/reasoning_effort_override.rs index 172d420230..3d0a966e9f 100644 --- a/codex-rs/core/tests/suite/reasoning_effort_override.rs +++ b/codex-rs/core/tests/suite/reasoning_effort_override.rs @@ -502,28 +502,26 @@ async fn reasoning_effort_override_disabled_on_resume_retires_update_at_compacti -> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; - let mut mocks = Vec::new(); - for (id, reply) in [ - ("initial", "initial reply"), - ("resumed", "resumed reply"), - ("after", "after compaction reply"), - ] { - mocks.push( - responses::mount_sse_once( - &server, - responses::sse(vec![ - responses::ev_assistant_message(id, reply), - responses::ev_completed(id), - ]), - ) - .await, - ); - } - let compact = responses::mount_compact_json_once( + let reply = |id, text| { + responses::sse(vec![ + responses::ev_assistant_message(id, text), + responses::ev_completed(id), + ]) + }; + let mock = responses::mount_sse_sequence( &server, - serde_json::json!({ - "output": [{"type": "compaction", "encrypted_content": "compacted-history"}] - }), + vec![ + reply("initial", "initial reply"), + reply("resumed", "resumed reply"), + responses::sse(vec![ + serde_json::json!({ + "type": "response.output_item.done", + "item": {"type": "compaction", "encrypted_content": "compacted-history"}, + }), + responses::ev_completed("compact"), + ]), + reply("after", "after compaction reply"), + ], ) .await; let initial = override_builder().build_with_auto_env(&server).await?; @@ -536,10 +534,6 @@ async fn reasoning_effort_override_disabled_on_resume_retires_update_at_compacti .features .disable(Feature::ReasoningEffortOverride) .expect("disable overrides"); - config - .features - .disable(Feature::RemoteCompactionV2) - .expect("disable remote compaction v2"); config.model_reasoning_effort = Some(ReasoningEffort::High); config.chatgpt_base_url = chatgpt_base_url; }) @@ -553,12 +547,7 @@ async fn reasoning_effort_override_disabled_on_resume_retires_update_at_compacti .await; resumed.submit_text_turn("after compaction").await?; - let requests = [ - mocks[0].single_request(), - mocks[1].single_request(), - compact.single_request(), - mocks[2].single_request(), - ]; + let requests = mock.requests(); assert_eq!( requests .iter() @@ -832,125 +821,9 @@ async fn reasoning_effort_override_resume_refreshes_selected_effort( Ok(()) } -#[test_case(false; "success")] -#[test_case(true; "failure")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn reasoning_effort_override_remote_v1_compaction_uses_pin( - fail_compaction: bool, -) -> anyhow::Result<()> { - skip_if_no_network!(Ok(())); - let server = responses::start_mock_server().await; - let first = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("first")]), - ) - .await; - let after = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("after")]), - ) - .await; - let unchanged = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("unchanged")]), - ) - .await; - let changed = responses::mount_sse_once( - &server, - responses::sse(vec![responses::ev_completed("changed")]), - ) - .await; - let compact = responses::mount_compact_response_once( - &server, - if fail_compaction { - ResponseTemplate::new(400).set_body_json(serde_json::json!({ - "error": {"code": "invalid_request_error", "message": "compaction failed"} - })) - } else { - ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "output": [{"type": "compaction", "encrypted_content": "compacted-history"}] - })) - }, - ) - .await; - let chatgpt_base_url = format!("{}/backend-api", server.uri()); - let test = override_builder() - .with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing()) - .with_config(move |config| { - config.chatgpt_base_url = chatgpt_base_url; - config - .features - .disable(Feature::RemoteCompactionV2) - .expect("disable remote compaction v2"); - }) - .build_with_auto_env(&server) - .await?; - test.submit_text_turn("first").await?; - submit_thread_settings( - &test.codex, - ThreadSettingsOverrides { - effort: Some(Some(ReasoningEffort::High)), - ..Default::default() - }, - ) - .await?; - test.codex.submit(Op::Compact).await?; - if fail_compaction { - wait_for_event(&test.codex, |event| matches!(event, EventMsg::Error(_))).await; - } - wait_for_event(&test.codex, |event| { - matches!(event, EventMsg::TurnComplete(_)) - }) - .await; - test.submit_text_turn("after compaction").await?; - test.submit_text_turn("unchanged effort").await?; - submit_thread_settings( - &test.codex, - ThreadSettingsOverrides { - effort: Some(Some(ReasoningEffort::Low)), - ..Default::default() - }, - ) - .await?; - test.submit_text_turn("changed effort").await?; - let medium = effort_update(ReasoningEffort::Medium); - let after_updates = if fail_compaction { - vec![medium.clone(), effort_update(ReasoningEffort::High)] - } else { - Vec::new() - }; - let mut changed_updates = after_updates.clone(); - changed_updates.push(effort_update(ReasoningEffort::Low)); - let after_effort = Value::from(if fail_compaction { "medium" } else { "high" }); - assert_eq!( - [ - first.single_request(), - compact.single_request(), - after.single_request(), - unchanged.single_request(), - changed.single_request(), - ] - .map(|request| ( - request.body_json()["reasoning"]["effort"].clone(), - effort_updates(&request), - )), - [ - (Value::from("medium"), vec![medium.clone()]), - (Value::from("medium"), vec![medium]), - (after_effort.clone(), after_updates.clone()), - (after_effort.clone(), after_updates), - (after_effort, changed_updates), - ], - ); - Ok(()) -} - -#[test_case(false; "remote v1")] -#[test_case(true; "remote v2")] -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn reasoning_effort_override_compaction_fallback_uses_each_models_effort( - remote_v2: bool, -) -> anyhow::Result<()> { +async fn reasoning_effort_override_compaction_fallback_uses_each_models_effort() +-> anyhow::Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let first = responses::mount_sse_once( @@ -969,30 +842,17 @@ async fn reasoning_effort_override_compaction_fallback_uses_each_models_effort( let failure = ResponseTemplate::new(/*s*/ 400).set_body_json(serde_json::json!({ "error": {"message": "previous model cannot compact this history"} })); - let compactions = if remote_v2 { - responses::mount_response_sequence( - &server, - vec![ - failure, - responses::sse_response(responses::sse(vec![ - serde_json::json!({"type": "response.output_item.done", "item": compaction}), - responses::ev_completed("fallback"), - ])), - ], - ) - .await - } else { - responses::mount_compact_response_sequence( - &server, - vec![ - failure, - ResponseTemplate::new(/*s*/ 200).set_body_json(serde_json::json!({ - "output": [compaction] - })), - ], - ) - .await - }; + let compactions = responses::mount_response_sequence( + &server, + vec![ + failure, + responses::sse_response(responses::sse(vec![ + serde_json::json!({"type": "response.output_item.done", "item": compaction}), + responses::ev_completed("fallback"), + ])), + ], + ) + .await; let after = responses::mount_sse_once( &server, responses::sse(vec![responses::ev_completed("after")]), @@ -1012,17 +872,6 @@ async fn reasoning_effort_override_compaction_fallback_uses_each_models_effort( .with_config(move |config| { config.chatgpt_base_url = chatgpt_base_url; config.model_provider.stream_max_retries = Some(0); - if remote_v2 { - config - .features - .enable(Feature::RemoteCompactionV2) - .expect("enable remote compaction v2"); - } else { - config - .features - .disable(Feature::RemoteCompactionV2) - .expect("disable remote compaction v2"); - } }) .build_with_auto_env(&server) .await?; diff --git a/codex-rs/core/tests/suite/responses_lite.rs b/codex-rs/core/tests/suite/responses_lite.rs index 734104e230..8ffe25e982 100644 --- a/codex-rs/core/tests/suite/responses_lite.rs +++ b/codex-rs/core/tests/suite/responses_lite.rs @@ -547,24 +547,30 @@ async fn responses_lite_compact_request_uses_lite_transport_contract() -> Result skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; - let response_mock = responses::mount_sse_once( + let response_mock = responses::mount_sse_sequence( &server, - responses::sse(vec![ - responses::ev_response_created("resp-1"), - responses::ev_completed("resp-1"), - ]), + vec![ + responses::sse(vec![ + responses::ev_response_created("resp-1"), + responses::ev_completed("resp-1"), + ]), + responses::sse(vec![ + serde_json::json!({ + "type": "response.output_item.done", + "item": { + "type": "compaction", + "encrypted_content": "RESPONSES_LITE_COMPACT_SUMMARY", + } + }), + responses::ev_completed("resp-compact"), + ]), + ], ) .await; - let compact_mock = - responses::mount_compact_json_once(&server, serde_json::json!({ "output": [] })).await; - let mut builder = test_codex() - .with_model_info_override("gpt-5.4", |model_info| { - model_info.use_responses_lite = true; - }) - .with_config(|config| { - let _ = config.features.disable(Feature::RemoteCompactionV2); - }); + let mut builder = test_codex().with_model_info_override("gpt-5.4", |model_info| { + model_info.use_responses_lite = true; + }); let test = builder.build(&server).await?; test.submit_turn("Compact this conversation").await?; @@ -574,8 +580,14 @@ async fn responses_lite_compact_request_uses_lite_transport_contract() -> Result }) .await; - response_mock.single_request(); - let compact_request = compact_mock.single_request(); + let requests = response_mock.requests(); + assert_eq!(requests.len(), 2); + let compact_request = &requests[1]; + assert_eq!(compact_request.path(), "/v1/responses"); + assert_eq!( + compact_request.inputs_of_type("compaction_trigger").len(), + 1 + ); assert_eq!( compact_request.header(RESPONSES_LITE_HEADER).as_deref(), Some("true") diff --git a/codex-rs/core/tests/suite/step_settings_compaction.rs b/codex-rs/core/tests/suite/step_settings_compaction.rs index 473af1d235..629f115d3d 100644 --- a/codex-rs/core/tests/suite/step_settings_compaction.rs +++ b/codex-rs/core/tests/suite/step_settings_compaction.rs @@ -9,12 +9,10 @@ use test_case::test_case; #[derive(Clone, Copy)] enum CompactionMode { Local, - Remote, RemoteV2, } #[test_case(CompactionMode::Local; "local")] -#[test_case(CompactionMode::Remote; "remote")] #[test_case(CompactionMode::RemoteV2; "remote v2")] #[tokio::test] async fn compaction_preserves_updated_summary(mode: CompactionMode) -> Result<()> { @@ -32,23 +30,16 @@ async fn compaction_preserves_updated_summary(mode: CompactionMode) -> Result<() responses::ev_completed_with_tokens("plan-response", /*total_tokens*/ 330_000), ]), ]; - let compact = match mode { - CompactionMode::Local => { - bodies.push(sse(vec![ - responses::ev_assistant_message("summary", "Compacted history"), - ev_completed("compact"), - ])); - None - } - CompactionMode::Remote => Some( - responses::mount_compact_user_history_with_summary_once(&server, "encrypted-summary") - .await, - ), - CompactionMode::RemoteV2 => { - bodies.push(sse(vec![json!({"type": "response.output_item.done", "item": {"type": "compaction", "encrypted_content": "encrypted-summary"}}), ev_completed("compact")])); - None - } - }; + bodies.push(match mode { + CompactionMode::Local => sse(vec![ + responses::ev_assistant_message("summary", "Compacted history"), + ev_completed("compact"), + ]), + CompactionMode::RemoteV2 => sse(vec![ + json!({"type": "response.output_item.done", "item": {"type": "compaction", "encrypted_content": "encrypted-summary"}}), + ev_completed("compact"), + ]), + }); bodies.push(sse_completed("done")); let responses = mount_sse_sequence(&server, bodies).await; let test = step_settings_test() @@ -61,20 +52,9 @@ async fn compaction_preserves_updated_summary(mode: CompactionMode) -> Result<() .expect("disable token budget"); config.model_provider.name = match mode { CompactionMode::Local => "Local compaction test", - CompactionMode::Remote | CompactionMode::RemoteV2 => "OpenAI", + CompactionMode::RemoteV2 => "OpenAI", } .to_string(); - if matches!(mode, CompactionMode::RemoteV2) { - config - .features - .enable(Feature::RemoteCompactionV2) - .expect("enable v2"); - } else { - config - .features - .disable(Feature::RemoteCompactionV2) - .expect("disable v2"); - } }) .build_with_auto_env(&server) .await?; @@ -106,9 +86,6 @@ async fn compaction_preserves_updated_summary(mode: CompactionMode) -> Result<() ], [json!("concise"), json!("detailed")] ); - if let Some(compact) = compact { - compact.single_request(); - } let rollout = std::fs::read_to_string(test.session_configured.rollout_path.expect("rollout path"))?; let items = rollout diff --git a/codex-rs/core/tests/suite/subagent_notifications.rs b/codex-rs/core/tests/suite/subagent_notifications.rs index 37c18583c0..2e4a27ddf6 100644 --- a/codex-rs/core/tests/suite/subagent_notifications.rs +++ b/codex-rs/core/tests/suite/subagent_notifications.rs @@ -1297,10 +1297,6 @@ async fn grandchild_full_fork_preserves_context_baseline( config.update_plan_enabled = true; // Use local compaction so the test controls the replacement history. config.model_provider.name = "test-provider".to_string(); - config - .features - .disable(Feature::RemoteCompactionV2) - .expect("test config should allow feature update"); config.compact_prompt = Some(COMPACT_PROMPT.to_string()); config.model_auto_compact_token_limit = Some(200_000); config.model_context_window = Some(1_000_000); diff --git a/codex-rs/core/tests/suite/token_budget.rs b/codex-rs/core/tests/suite/token_budget.rs index 3828adf59a..e8add8d490 100644 --- a/codex-rs/core/tests/suite/token_budget.rs +++ b/codex-rs/core/tests/suite/token_budget.rs @@ -37,7 +37,6 @@ use core_test_support::responses::ev_completed_with_tokens; use core_test_support::responses::ev_exec_command_call; use core_test_support::responses::ev_function_call; use core_test_support::responses::ev_response_created; -use core_test_support::responses::mount_compact_json_once; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_sequence; use core_test_support::responses::sse; @@ -1058,8 +1057,6 @@ async fn token_budget_context_uses_new_window_after_compaction( ], ) .await; - let compact = mount_compact_json_once(&server, json!({ "output": [] })).await; - let mut model_provider = built_in_model_providers(/*openai_base_url*/ None)["openai"].clone(); model_provider.base_url = Some(format!("{}/v1", server.uri())); model_provider.supports_websockets = false; @@ -1098,10 +1095,6 @@ async fn token_budget_context_uses_new_window_after_compaction( let requests = responses.requests(); assert_eq!(requests.len(), 2); - assert!( - compact.requests().is_empty(), - "token budget compaction should not call server-side compaction" - ); let initial_token_budget = token_budget_contexts(&requests[0]); assert_eq!(initial_token_budget.len(), 1); diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 5b5206047c..2ce9cb924b 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -588,7 +588,7 @@ impl Features { "js_repl_tools_only" => { continue; } - "remote_control" => { + "remote_control" | "remote_compaction_v2" => { continue; } "apply_patch_freeform" => { @@ -1732,8 +1732,8 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::RemoteCompactionV2, key: "remote_compaction_v2", - stage: Stage::Stable, - default_enabled: true, + stage: Stage::Removed, + default_enabled: false, }, FeatureSpec { id: Feature::CompactionImageBudget,