Roll over Guardian context before follow-up reviews (#41215)

## Why

Long-lived Guardian sessions can exhaust the review model's context window. After
a rollover, a transcript delta is insufficient because the new window no longer
contains the original transcript or persistent review instructions.

## What changed

- Compact an exhausted Guardian session before submitting the next review while
  respecting the review model's token-budget configuration.
- Detect whether the full transcript remains in the reviewer history. If it does
  not, reset transcript and Node REPL evidence cursors so the next review replays
  the complete transcript and associated image evidence.
- Restore the follow-up reminder and Node REPL policy when compaction removes
  them.

## Testing

- Cover rollover both when the reviewer window is already exhausted and when a
  follow-up reminder crosses the limit.
- Verify that multimodal reviews preserve the fallback buffer and replay browser
  evidence after rollover.

GitOrigin-RevId: f2a02a84e1f25d15c6e2b8eddb93e2d1805449f5
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-08-27 22:28:46 +00:00
1 parent f6494dc8f5
commit bae69125df
5 files changed
+405 -88

No files matched your search

+65 -4
View File
@@ -4508,6 +4508,8 @@ await tools.exec_command({ cmd: "true", sandbox_permissions: "require_escalated"
#[test_case("node_repl", true, true, false, Some("unbounded"); "text_fallback_without_context_bound")]
#[test_case("node_repl", true, true, false, Some("small"); "text_fallback_with_insufficient_context")]
#[test_case("node_repl", true, true, false, Some("large_prompt"); "images_resume_after_prompt_pressure")]
#[test_case("node_repl", true, true, false, Some("buffered"); "multimodal_reviewer_preserves_model_owned_fallback_buffer")]
#[test_case("node_repl", true, true, false, Some("rollover"); "multimodal_reviewer_rollover_replays_evidence")]
#[test_case("cua_repl", false, false, false, None; "cua_disabled")]
#[test_case("cua_repl", true, false, false, None; "cua_manually_enabled_text_only")]
#[test_case("cua_repl", true, true, false, None; "cua_manually_enabled_multimodal")]
@@ -4537,6 +4539,8 @@ async fn code_mode_node_repl_text_evidence_is_visible_only_to_guardian(
const PRIVATE_IMAGE: &str = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/iZk9HQAAAABJRU5ErkJggg==";
let server = responses::start_mock_server().await;
let mcp_server_bin = remote_aware_stdio_server_bin()?;
let reviewer_rollover = reviewer_constraint == Some("rollover");
let reviewer_token_budget = matches!(reviewer_constraint, Some("buffered" | "rollover"));
let check_detail = enhanced_transcripts && transcript_images && reviewer_constraint.is_none();
let mut large_image = Cursor::new(Vec::new());
if check_detail {
@@ -4559,7 +4563,11 @@ async fn code_mode_node_repl_text_evidence_is_visible_only_to_guardian(
.iter_mut()
.find(|model| model.slug == "gpt-5.6-luna")
.expect("API-key Guardian reviewer");
if check_detail {
if reviewer_token_budget {
reviewer.context_window = Some(100_000);
reviewer.max_context_window = Some(100_000);
reviewer.auto_compact_token_limit = Some(50_000);
} else if check_detail {
reviewer.use_responses_lite = false;
} else if reviewer_constraint == Some("unsupported") {
reviewer.input_modalities =
@@ -4574,6 +4582,12 @@ async fn code_mode_node_repl_text_evidence_is_visible_only_to_guardian(
.features
.enable(Feature::CodeMode)
.expect("enable Code Mode");
if reviewer_token_budget {
config
.features
.enable(Feature::TokenBudget)
.expect("enable token budget");
}
config
.features
.set_enabled(
@@ -4609,7 +4623,8 @@ async fn code_mode_node_repl_text_evidence_is_visible_only_to_guardian(
let test = builder.build_with_auto_env(&server).await?;
wait_for_mcp_server(&test.codex, repl_server).await?;
let images_enabled = auto_review_required || (enhanced_transcripts && transcript_images);
let reviewer_images = images_enabled && reviewer_constraint.is_none();
let reviewer_images =
images_enabled && (reviewer_constraint.is_none() || reviewer_token_budget);
let snapshot_padding = if images_enabled && reviewer_constraint != Some("large_prompt") {
2_500
} else {
@@ -4638,12 +4653,13 @@ await tools.mcp__node_repl__js({ code: 'nodeRepl.empty()' });
await tools.mcp__node_repl__js({ code: 'await nodeRepl.emitImage(await tab.screenshot())' });
if (LARGE_IMAGE) await tools.mcp__node_repl__image_scenario({ scenario: "invalid_image_bytes_then_image" });
await tools.exec_command({ cmd: "true", sandbox_permissions: "require_escalated", justification: "review" });
await tools.mcp__node_repl__js({ code: 'await nodeRepl.emitImage(await tab.screenshot())' });
if (!REVIEWER_ROLLOVER) await tools.mcp__node_repl__js({ code: 'await nodeRepl.emitImage(await tab.screenshot())' });
if (LARGE_IMAGE) await tools.mcp__node_repl__image({});
await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_escalated", justification: "review again" });
"#
.replace("node_repl", repl_server)
.replace("SNAPSHOT_PADDING", &snapshot_padding.to_string())
.replace("REVIEWER_ROLLOVER", &reviewer_rollover.to_string())
.replace("LARGE_IMAGE", &check_detail.to_string());
let response_mock = responses::mount_sse_sequence(
&server,
@@ -4672,7 +4688,14 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e
]),
sse(vec![
ev_assistant_message("guardian", r#"{"outcome":"allow"}"#),
ev_completed("resp-guardian"),
if reviewer_token_budget {
responses::ev_completed_with_tokens(
"resp-guardian",
/*total_tokens*/ if reviewer_rollover { 70_000 } else { 60_000 },
)
} else {
ev_completed("resp-guardian")
},
]),
sse(vec![
ev_assistant_message("guardian-again", r#"{"outcome":"allow"}"#),
@@ -4773,6 +4796,44 @@ await tools.exec_command({ cmd: "printf second", sandbox_permissions: "require_e
reviewer_image_urls
}
);
if reviewer_rollover {
let second_request = guardian_requests[1];
let second_prompt = second_request
.message_input_text_groups("user")
.last()
.expect("post-rollover Guardian review prompt")
.join("");
assert!(second_prompt.contains(">>> TRANSCRIPT START\n"));
assert!(!second_prompt.contains(">>> TRANSCRIPT DELTA START\n"));
assert!(second_prompt.contains(NODE_REPL_DOM_MIDDLE));
assert_eq!(
second_request.message_input_image_urls("user"),
vec![PRIVATE_IMAGE.to_string()],
"browser screenshots must be replayed into the fresh reviewer window"
);
assert!(
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Previous context window id:")),
"Guardian should roll over before rebuilding browser evidence"
);
} else if reviewer_token_budget {
let second_request = guardian_requests[1];
let second_prompt = second_request
.message_input_text_groups("user")
.last()
.expect("buffered Guardian review prompt")
.join("");
assert!(second_prompt.contains(">>> TRANSCRIPT DELTA START\n"));
assert!(
second_request
.message_input_texts("developer")
.iter()
.all(|text| !text.contains("Previous context window id:")),
"Guardian should preserve the reviewer model's fallback buffer before rolling over"
);
}
let parent_request = requests.last().expect("parent turn should complete");
let parent_input = serde_json::to_string(&parent_request.input())?;
assert!(!parent_input.contains("data:image/png;base64,"));
@@ -52,6 +52,7 @@ use core_test_support::responses::assert_parent_turn;
use core_test_support::responses::assert_root_turn;
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::ev_function_call_with_namespace;
use core_test_support::responses::ev_response_created;
@@ -234,6 +235,117 @@ async fn guardian_session_inherits_parent_http_fallback(
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[test_case(60_000; "reviewer_window_is_already_exhausted")]
#[test_case(49_990; "followup_reminder_exhausts_reviewer_window")]
async fn guardian_review_resends_full_transcript_after_reviewer_context_rollover(
first_review_total_tokens: i64,
) -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_wine_exec!(
Ok(()),
"Guardian approval actions require host-native paths"
);
let server = start_mock_server().await;
let mut builder = test_codex()
.with_model_info_override("gpt-5.5", |model| {
model.auto_review_model_override = Some(model.slug.clone());
})
.with_config(|config| {
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);
config.approvals_reviewer = ApprovalsReviewer::AutoReview;
config
.features
.enable(Feature::TokenBudget)
.expect("test config should allow token budget");
});
let test = builder.build_with_auto_env(&server).await?;
let command = json!({
"cmd": "true",
"sandbox_permissions": "require_escalated",
"justification": "Read the internal samples the user authorized.",
})
.to_string();
let approval = r#"{"risk_level":"low","user_authorization":"high","outcome":"allow"}"#;
let responses = mount_sse_sequence(
&server,
vec![
sse(vec![
ev_response_created("resp-parent-first"),
ev_function_call("exec-first", "exec_command", &command),
ev_completed("resp-parent-first"),
]),
sse(vec![
ev_response_created("resp-guardian-first"),
ev_assistant_message("guardian-first", approval),
ev_completed_with_tokens("resp-guardian-first", first_review_total_tokens),
]),
sse(vec![
ev_response_created("resp-parent-second"),
ev_function_call("exec-second", "exec_command", &command),
ev_completed("resp-parent-second"),
]),
sse(vec![
ev_response_created("resp-guardian-second"),
ev_assistant_message("guardian-second", approval),
ev_completed("resp-guardian-second"),
]),
sse(vec![
ev_response_created("resp-parent-done"),
ev_assistant_message("parent-done", "done"),
ev_completed("resp-parent-done"),
]),
],
)
.await;
let user_authorization = "Read the internal evaluation samples I have authorized.";
test.submit_text_turn(user_authorization).await?;
let requests = responses.requests();
let guardian_requests = requests
.iter()
.filter(|request| {
request.body_json()["client_metadata"]["x-openai-subagent"].as_str() == Some("guardian")
})
.collect::<Vec<_>>();
assert_eq!(guardian_requests.len(), 2);
assert_eq!(
guardian_requests[0].body_json()["client_metadata"]["thread_id"],
guardian_requests[1].body_json()["client_metadata"]["thread_id"],
"the same Guardian reviewer should survive the context-window rollover"
);
let second_request = guardian_requests[1];
let second_prompt = second_request
.message_input_text_groups("user")
.last()
.expect("post-rollover Guardian review prompt")
.join("");
assert!(
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Previous context window id:")),
"Guardian should have rolled into a new context window"
);
assert!(second_prompt.contains(">>> TRANSCRIPT START\n"));
assert!(!second_prompt.contains(">>> TRANSCRIPT DELTA START\n"));
assert!(second_prompt.contains(user_authorization));
assert!(
second_request
.message_input_texts("developer")
.iter()
.any(|text| text.contains("Use prior reviews as context, not binding precedent.")),
"the follow-up policy reminder should survive reviewer context rollover"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[test_case(CodexAuth::from_api_key("test-api-key"), "gpt-5.6-luna"; "api_key_uses_luna_with_responses_lite")]
#[test_case(CodexAuth::create_dummy_chatgpt_auth_for_testing(), "codex-auto-review"; "chatgpt_uses_codex_auto_review")]