Preserve independent Guardian history across parent compaction (#48779)

## Why

Guardian reviews need to retain original evidence across parent compaction when checkpoint reuse is disabled, including after resume and rollback.

## What changed

- Make disabling `guardian_reuse_parent_compaction` select a bounded, independent review transcript while preserving thread-owned authorization.
- Keep synchronous reviews and asynchronous scoring independent of parent checkpoints, and allow reviewer sessions to continue using transcript deltas across parent compaction.
- Persist transcript entries with rollback provenance while remaining compatible with older checkpoint readers and metadata-free checkpoints.
- Exclude compaction output from the transcript and synthetic summaries from rollback turn boundaries. Use acceptance ordering to remove rolled-back evidence even when persistence order differs.

## Testing

Add regression coverage for reviewer continuity, evidence retention across compaction and resume, rollback after local and remote compaction, and checkpoint serialization compatibility.

GitOrigin-RevId: fea0852cc5537372854f4a34bacb6bfda6d13e88
This commit is contained in:
felixxia-oai
2026-09-27 20:15:23 +00:00
committed by copyberry
parent 89bf86d0bd
commit 21eb35513d
29 changed files with 523 additions and 140 deletions
@@ -68,6 +68,7 @@ enum EvidenceSize {
enum CheckpointReuse {
Enabled,
Disabled,
Independent,
}
#[derive(Clone, Copy)]
@@ -79,6 +80,8 @@ enum ReviewCheckpoint {
EmptyReviewerHash,
}
#[test_case(CheckpointReuse::Independent, Some("matching"), Some("different"), 0, EvidenceSize::Normal, ReviewCheckpoint::Valid; "independent transcript ignores incompatible checkpoints")]
#[test_case(CheckpointReuse::Independent, Some("matching"), Some("matching"), 0, EvidenceSize::Normal, ReviewCheckpoint::Valid; "independent transcript overrides Luna checkpoint reuse")]
#[test_case(CheckpointReuse::Enabled, Some("matching"), Some("different"), 0, EvidenceSize::Normal, ReviewCheckpoint::EmptyContent; "empty checkpoint fails closed")]
#[test_case(CheckpointReuse::Enabled, Some("matching"), Some("different"), 0, EvidenceSize::Normal, ReviewCheckpoint::DifferentReviewerHash; "different sync hash preserves retained evidence")]
#[test_case(CheckpointReuse::Enabled, Some("matching"), Some("different"), 0, EvidenceSize::Normal, ReviewCheckpoint::UnknownReviewer; "unknown sync hash preserves retained evidence")]
@@ -107,10 +110,11 @@ async fn guardians_retain_evidence_after_compaction_and_resume(
const RESTRICTION: &str = "Only publish to a private repository.";
const EVIDENCE: &str = "repository is private";
const SUMMARY: &str = "Repository inspection was summarized.";
let reuse_parent_compaction = matches!(checkpoint_reuse, CheckpointReuse::Enabled);
let reuse_parent_compaction = !matches!(checkpoint_reuse, CheckpointReuse::Disabled);
let compatible =
reuse_parent_compaction && parent_hash == Some("matching") && luna_hash == parent_hash;
let requires_sync = !compatible;
let independent = matches!(checkpoint_reuse, CheckpointReuse::Independent);
let requires_sync = !compatible && !independent;
let oversized_instruction = matches!(evidence_size, EvidenceSize::OversizedInstruction);
let reviewer_hash = match review_checkpoint {
ReviewCheckpoint::DifferentReviewerHash => Some("different-reviewer"),
@@ -260,10 +264,14 @@ async fn guardians_retain_evidence_after_compaction_and_resume(
.enable_feature(Feature::GuardianApproval)
.disable_feature(Feature::EnableRequestCompression)
.disable_feature(Feature::TokenBudget)
.disable_feature(Feature::GuardianReuseParentCompaction)
.with_extra_config(&format!(
"[mcp_servers.{TEST_SERVER_NAME}]\nurl = \"{mcp_url}/mcp\"\ndefault_tools_approval_mode = \"prompt\"\n\n[features.guardianv2]\nenabled = true\npersist_scores = true\nreuse_parent_compaction = {reuse_parent_compaction}\n\n[features.guardianv2.review_scope]\ncomputer_use_only = false"
));
let mock_config = if independent {
mock_config.disable_feature(Feature::GuardianReuseParentCompaction)
} else {
mock_config
};
mock_config.write(codex_home.path())?;
let config = load_default_config_for_test(&codex_home).await;
let models = [
@@ -524,8 +532,11 @@ async fn guardians_retain_evidence_after_compaction_and_resume(
let review = &reviews[index];
let sync_input = review["input"].as_array().expect("request input array");
let async_input = request["input"].as_array().expect("request input array");
assert_eq!(sync_input.contains(&checkpoint), index > 0);
assert_eq!(async_input.contains(&checkpoint), index > 0 && compatible);
assert_eq!(sync_input.contains(&checkpoint), index > 0 && !independent);
assert_eq!(
async_input.contains(&checkpoint),
index > 0 && compatible && !independent
);
let sync_text = sync_input
.iter()
.filter(|item| item["role"] == "user")
@@ -560,9 +571,10 @@ async fn guardians_retain_evidence_after_compaction_and_resume(
}));
{
assert!(sync_text.contains(RESTRICTION));
assert!(
!sync_text.contains(&expected_output),
"raw tool result must not survive the parent checkpoint"
assert_eq!(
sync_text.contains(&expected_output),
independent,
"only independent review retains raw tool evidence"
);
assert!(sync_text.contains(">>> TRANSCRIPT START"));
assert!(!sync_text.contains(">>> TRANSCRIPT DELTA START"));
@@ -590,9 +602,10 @@ async fn guardians_retain_evidence_after_compaction_and_resume(
content.contains(RESTRICTION),
"retained instruction must survive"
);
assert!(
!transcript.contains(&expected_output),
"raw tool result must not survive the parent checkpoint"
assert_eq!(
transcript.contains(&expected_output),
independent,
"only independent scoring retains raw tool evidence"
);
assert!(transcript.contains(RESTRICTION));
assert!(!transcript.contains(SUMMARY));
@@ -681,7 +694,11 @@ async fn guardians_retain_evidence_after_compaction_and_resume(
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
{
if line["type"] == "compacted" {
assert!(line["payload"]["guardian_history"].is_null());
let saved_review = &line["payload"]["guardian_history"];
assert_eq!(saved_review.is_null(), !independent);
if independent {
assert!(saved_review.to_string().contains(EVIDENCE));
}
}
}
let items = rollout