mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Harden Guardian reviews after context compaction (#42852)
## Why Guardian reviews must not lose user authorization constraints or reuse an unreadable or incompatible parent checkpoint after compaction. ## What changed - Retain bounded excerpts of oversized root user messages across compaction, resume, and rollback, and recover excerpts from legacy Guardian history when available. - Require thread-owned Guardian review sessions and asynchronous scoring to reuse only non-empty checkpoints produced by a compatible review model. - Fail closed instead of reviewing without the required checkpoint when reuse is disabled, compatibility is unknown, or checkpoint content is unusable. ## Testing Expanded retained-context, review-session, asynchronous-scoring, and app-server coverage for oversized instructions, legacy recovery, model incompatibility, missing content, resume, rollback, and disabled checkpoint reuse. GitOrigin-RevId: f30d837197e0d838e656a632109f490c38208f5e
This commit is contained in:
@@ -324,7 +324,13 @@ impl ApprovalReviewContributor for GuardianV2Extension {
|
||||
let history = thread.conversation_history_snapshot().await;
|
||||
if thread_context_enabled {
|
||||
let sampler = thread_store.get::<LunaSampler>()?;
|
||||
if requires_sync_for_compaction(&guardian_config, history.as_ref(), &sampler) {
|
||||
if requires_sync_for_compaction(&guardian_config, history.as_ref(), &sampler)
|
||||
|| encrypted_parent_compaction(
|
||||
history.items(),
|
||||
guardian_config.max_parent_compaction_tokens,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
thread_store.insert(StrictReviewReason::IncompatibleCompaction);
|
||||
record_fast_decision(
|
||||
extension_metrics.as_deref(),
|
||||
@@ -635,6 +641,7 @@ impl GuardianV2Extension {
|
||||
guardian_config.max_parent_compaction_tokens,
|
||||
) {
|
||||
Ok(compaction) => compaction,
|
||||
Err(ParentCompactionError::Unusable) if !thread_context_enabled => None,
|
||||
Err(_) => {
|
||||
Self::record_fail_closed_score(input.thread_store, sampled_at);
|
||||
record_classification(
|
||||
@@ -967,6 +974,7 @@ impl GuardianV2Extension {
|
||||
enum ParentCompactionError {
|
||||
Serialization,
|
||||
Oversized,
|
||||
Unusable,
|
||||
}
|
||||
|
||||
// Sampling and fast approval must apply the same checkpoint eligibility policy.
|
||||
@@ -975,18 +983,17 @@ pub(super) fn requires_sync_for_compaction(
|
||||
history: &dyn ConversationHistorySnapshot,
|
||||
sampler: &LunaSampler,
|
||||
) -> bool {
|
||||
config.reuse_parent_compaction
|
||||
&& history.items().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. }
|
||||
)
|
||||
})
|
||||
&& !sampler.supports_parent_compaction(history.latest_compaction_model_hash())
|
||||
history.items().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. }
|
||||
)
|
||||
}) && (!config.reuse_parent_compaction
|
||||
|| !sampler.supports_parent_compaction(history.latest_compaction_model_hash()))
|
||||
}
|
||||
|
||||
// An unusable latest compaction must never fall back to an older one. Missing
|
||||
// encrypted content can be omitted; content that cannot be bounded rejects the sample.
|
||||
// encrypted content is rejected here; only legacy callers may omit that checkpoint.
|
||||
fn encrypted_parent_compaction<'a>(
|
||||
items: impl Iterator<Item = &'a ResponseItem>,
|
||||
max_parent_compaction_tokens: usize,
|
||||
@@ -1015,10 +1022,10 @@ fn encrypted_parent_compaction<'a>(
|
||||
encrypted_content: Some(encrypted_content),
|
||||
..
|
||||
} => encrypted_content,
|
||||
_ => return Ok(None),
|
||||
_ => return Err(ParentCompactionError::Unusable),
|
||||
};
|
||||
if encrypted_content.is_empty() {
|
||||
return Ok(None);
|
||||
return Err(ParentCompactionError::Unusable);
|
||||
}
|
||||
let serialized = serde_json::to_vec(item).map_err(|_| ParentCompactionError::Serialization)?;
|
||||
if serialized.len() > max_compaction_bytes {
|
||||
|
||||
@@ -857,7 +857,7 @@ fn encrypted_parent_compaction_rejects_invalid_latest_item() {
|
||||
[&older, latest].into_iter(),
|
||||
DEFAULT_PARENT_COMPACTION_TOKENS,
|
||||
),
|
||||
Ok(None),
|
||||
Err(ParentCompactionError::Unusable),
|
||||
"an unusable latest summary must not resurrect older context"
|
||||
);
|
||||
}
|
||||
@@ -3335,7 +3335,7 @@ async fn assert_parent_compaction_reuse(thread_context_enabled: bool) -> Result<
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn contributor_can_disable_parent_compaction_reuse() -> Result<()> {
|
||||
async fn legacy_contributor_can_disable_parent_compaction_reuse() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let oversized_compaction = ResponseItem::Compaction {
|
||||
@@ -3347,7 +3347,7 @@ async fn contributor_can_disable_parent_compaction_reuse() -> Result<()> {
|
||||
oversized_compaction,
|
||||
user_instruction("Inspect the repository guidelines."),
|
||||
];
|
||||
let configuration = "[features]\nguardian_thread_context = true\n\n[features.guardianv2]\nenabled = true\nreuse_parent_compaction = false\nmax_parent_compaction_tokens = 256\n";
|
||||
let configuration = "[features]\nguardian_thread_context = false\n\n[features.guardianv2]\nenabled = true\nreuse_parent_compaction = false\nmax_parent_compaction_tokens = 256\n";
|
||||
let (request, test, _registry) = sample_configured_conversation_history(
|
||||
conversation_history,
|
||||
r#"{"path":"README.md"}"#,
|
||||
|
||||
Reference in New Issue
Block a user