From 478dbe9df0a33141d265db5977947cc432e7fe85 Mon Sep 17 00:00:00 2001 From: felixxia-oai Date: Thu, 20 Aug 2026 10:41:02 +0000 Subject: [PATCH] Make Guardian v2 parent compaction reuse configurable (#39691) ## What changed - Add `features.guardianv2.reuse_parent_compaction`, defaulting to `true` to preserve existing behavior. - When disabled, omit parent compaction items from Guardian v2 contributor requests. - Cover configuration parsing and disabled-reuse request construction. GitOrigin-RevId: 0cf7c0ac7a2567c9f73a3f9724df6f6a6170e995 --- codex-rs/core/config.schema.json | 3 + .../guardian-v2/src/async_scorer/config.rs | 2 + .../guardian-v2/src/async_scorer/extension.rs | 24 ++++---- .../src/async_scorer/extension_tests.rs | 57 +++++++++++++++++++ codex-rs/features/src/feature_configs.rs | 2 + codex-rs/features/src/tests.rs | 2 + 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index 3a7ba720ef..51eefd8e0a 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -1299,6 +1299,9 @@ "reasoning_effort": { "$ref": "#/definitions/ReasoningEffort" }, + "reuse_parent_compaction": { + "type": "boolean" + }, "review_scope": { "$ref": "#/definitions/GuardianV2ReviewScopeConfigToml" }, diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs index 96455a0bea..9fc6d949e5 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/config.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/config.rs @@ -32,6 +32,7 @@ pub(crate) struct GuardianV2Config { pub(crate) reasoning_effort: ReasoningEffort, pub(crate) max_action_tokens: usize, pub(crate) max_classifier_instruction_tokens: usize, + pub(crate) reuse_parent_compaction: bool, pub(crate) max_parent_compaction_tokens: usize, pub(crate) sandboxed_exec_commands: bool, pub(crate) transcript: TranscriptConfig, @@ -233,6 +234,7 @@ impl GuardianV2Config { reasoning_effort: configured.reasoning_effort.unwrap_or(ReasoningEffort::Low), max_action_tokens, max_classifier_instruction_tokens, + reuse_parent_compaction: configured.reuse_parent_compaction.unwrap_or(true), max_parent_compaction_tokens, sandboxed_exec_commands: configured .review_scope diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs index 41de621f79..5f57d76d34 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension.rs @@ -466,16 +466,20 @@ impl GuardianV2Extension { .latest_tool_call .fetch_add(/*val*/ 1, Ordering::Relaxed) .saturating_add(1); - let latest_parent_compaction = input - .conversation_history - .items() - .filter(|item| { - matches!( - item, - ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } - ) - }) - .last(); + let latest_parent_compaction = if guardian_config.reuse_parent_compaction { + input + .conversation_history + .items() + .filter(|item| { + matches!( + item, + ResponseItem::Compaction { .. } | ResponseItem::ContextCompaction { .. } + ) + }) + .last() + } else { + None + }; let parent_compaction = latest_parent_compaction.and_then(|item| { encrypted_parent_compaction( std::iter::once(item), diff --git a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs index b01a7ffdd4..eb46697ebf 100644 --- a/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs +++ b/codex-rs/ext/guardian-v2/src/async_scorer/extension_tests.rs @@ -1911,6 +1911,63 @@ async fn contributor_reuses_the_latest_compatible_parent_compaction() -> Result< Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn contributor_can_disable_parent_compaction_reuse() -> Result<()> { + skip_if_no_network!(Ok(())); + + let oversized_compaction = ResponseItem::Compaction { + id: Some(ResponseItemId::from_server("cmp_oversized".to_owned())), + encrypted_content: "a".repeat(TruncationPolicy::Tokens(/*limit*/ 256).byte_budget()), + internal_chat_message_metadata_passthrough: None, + }; + let conversation_history = vec![ + oversized_compaction, + ResponseItem::Message { + id: None, + role: "user".to_owned(), + content: vec![ContentItem::InputText { + text: "Inspect the repository guidelines.".to_owned(), + }], + phase: None, + internal_chat_message_metadata_passthrough: None, + }, + ]; + let configuration = "[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"}"#, + Some(TEST_GUARDIAN_POLICY), + configuration, + /*model_defaults*/ None, + ) + .await?; + + let input = request["input"] + .as_array() + .expect("Luna request input should be an array"); + assert_eq!(input.len(), 3); + assert_eq!(input[2]["role"], "user"); + assert!( + input + .iter() + .all(|item| item["type"] != "compaction" && item["type"] != "context_compaction") + ); + + let thread_store = test.codex.thread_extension_data(); + let score = tokio::time::timeout(Duration::from_secs(5), async { + loop { + if let Some(score) = thread_store.get::() { + return score; + } + tokio::task::yield_now().await; + } + }) + .await?; + assert_eq!(score.scores.get("action_risk"), Some(&0.8)); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn contributor_bounds_oversized_actions_and_fairly_truncates_nested_fields() -> Result<()> { skip_if_no_network!(Ok(())); diff --git a/codex-rs/features/src/feature_configs.rs b/codex-rs/features/src/feature_configs.rs index a39da8757a..c39e5938d6 100644 --- a/codex-rs/features/src/feature_configs.rs +++ b/codex-rs/features/src/feature_configs.rs @@ -139,6 +139,8 @@ pub struct GuardianV2ConfigToml { #[schemars(range(min = 100, max = 100000))] pub max_classifier_instruction_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] + pub reuse_parent_compaction: Option, + #[serde(skip_serializing_if = "Option::is_none")] #[schemars(range(min = 100, max = 100000))] pub max_parent_compaction_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index b89fe2ad5c..cc7e9fabbf 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -160,6 +160,7 @@ max_tool_call_lag = 2 reasoning_effort = "minimal" max_action_tokens = 512 max_classifier_instruction_tokens = 256 +reuse_parent_compaction = false max_parent_compaction_tokens = 384 [guardianv2.review_scope] @@ -187,6 +188,7 @@ max_recent_non_user_entries = 12 reasoning_effort: Some(codex_protocol::openai_models::ReasoningEffort::Minimal), max_action_tokens: Some(512), max_classifier_instruction_tokens: Some(256), + reuse_parent_compaction: Some(false), max_parent_compaction_tokens: Some(384), review_scope: Some(crate::GuardianV2ReviewScopeConfigToml { sandboxed_exec_commands: Some(true),