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
This commit is contained in:
rka-oai authored and copyberry committed 2026-09-09 18:04:04 +00:00
1 parent 2617ed2e1c
commit 3dc1e2a584
25 files changed
+675 -5635

No files matched your search

+48 -25
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>();
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"]));
+83 -295
View File
@@ -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<_>>(),
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() {
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -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::<serde_json::Result<Vec<ResponseItem>>>()?;
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::<serde_json::Result<Vec<ResponseItem>>>()?;
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(())
}
@@ -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<_>>(),
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)
+14 -9
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>();
assert_eq!(compact_requests.len(), 1);
let compact_request = compact_requests[0];
assert!(
compact_request
.message_input_texts("user")
-1
View File
@@ -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;
@@ -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?;
+28 -16
View File
@@ -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")
@@ -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
@@ -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);
@@ -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);