Preserve Guardian budget policy through review recovery (#58962)

## Why

Deduplicating against optional transcript entries can remove retained instructions or required tool calls even though the optional copies may later be evicted. Elastic instruction caps can also report evidence loss for revisions already delivered in full.

## What changed

- Capture the review's budget policy and use it for feasibility checks, admission, and restoration after compaction.
- Prefer required and protected native messages over optional duplicates, and use only complete, required pending transcript entries as proof of instruction delivery.
- Match elastic instruction duplicates by source revision and discard truncation observations for copies already delivered in full.

## Testing

Extend coverage for compaction recovery under fixed and elastic budgets, required and protected tool-call deduplication, and capped instruction duplicates covered by complete delivery metadata.

GitOrigin-RevId: fe456e645cdba6f782fb16ad582702d3b7ed012c
This commit is contained in:
ioanbudea-OAI
2026-11-02 20:10:12 +00:00
committed by copyberry
parent ba0dfd3897
commit 9ede2e87ce
15 changed files with 361 additions and 105 deletions
+57 -17
View File
@@ -3,6 +3,7 @@
//! recording. Budget failures preserve it for a compaction retry; successful
//! finalization consumes it. Checkpoint recovery skips startup compaction and
//! budgets the fresh full input against the preserved checkpoint.
//! Each review captures its section policy for feasibility, admission and recovery.
//! Loss telemetry describes accepted inputs, excluding feasibility checks and failed retries.
use std::collections::BTreeMap;
@@ -11,8 +12,10 @@ use std::sync::atomic::Ordering;
use codex_features::Feature;
use codex_guardian_context::ComposedContext;
use codex_guardian_context::HistoryTruncation;
use codex_guardian_context::PreparedElasticContext;
use codex_guardian_context::RequestBudget;
use codex_guardian_context::effective_input_token_limit;
use codex_history::ResponseItemEnvelope;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
@@ -33,6 +36,48 @@ use crate::session::turn_context::TurnContext;
#[derive(Clone)]
pub(crate) struct PendingReviewContext {
pub context: ComposedContext,
pub elastic: Option<codex_guardian_context::ElasticBudgetConfig>,
}
/// Delivery preparation finishes before either budget policy selects evidence.
pub(super) enum PreparedReviewContext<'a> {
Fixed(ComposedContext),
Elastic(PreparedElasticContext<'a>),
}
impl<'a> PreparedReviewContext<'a> {
pub(super) fn prepare(
mut context: ComposedContext,
elastic: Option<&'a codex_guardian_context::ElasticBudgetConfig>,
history: &[ResponseItemEnvelope],
) -> Result<Self, codex_guardian_context::SectionError> {
match elastic {
Some(config) => Ok(Self::Elastic(
context
.prepare_elastic_entries(config)?
.prepare_sync_delivery(history)?,
)),
None => {
codex_guardian_context::prepare_sync_delivery(&mut context, history)?;
Ok(Self::Fixed(context))
}
}
}
pub(super) fn enforce_budget(
self,
budget: RequestBudget,
history_truncation: HistoryTruncation,
) -> Result<ComposedContext, codex_guardian_context::SectionError> {
let notice = GuardianBudgetOmission
.render()
.into_text()
.unwrap_or_default();
match self {
Self::Fixed(context) => context.enforce_budget(budget, notice, history_truncation),
Self::Elastic(context) => context.enforce_budget(budget, notice),
}
}
}
pub(super) use codex_guardian_reviewer::CheckpointRecovery;
@@ -59,6 +104,7 @@ pub(crate) fn should_compact(session: &Session) -> CodexResult<bool> {
/// Frozen originals for this review, restored only if its live history is compacted.
pub(super) struct RetainedReviewContext {
pub context: ComposedContext,
pub elastic: Option<codex_guardian_context::ElasticBudgetConfig>,
pub history_version: u64,
}
@@ -78,7 +124,7 @@ pub(crate) async fn check_pending(session: &Session, turn: &TurnContext) -> Code
codex_protocol::protocol::TruncationPolicy::Bytes(base.text.len()).token_budget();
let maximum = effective_input_token_limit(turn.model_info(), turn.config.model_context_window)
.saturating_sub(super::request_budget::INPUT_TOKEN_MARGIN);
let mut context = pending.context.clone();
let context = pending.context.clone();
let history = session
.clone_history()
.await
@@ -87,19 +133,19 @@ pub(crate) async fn check_pending(session: &Session, turn: &TurnContext) -> Code
session.services.image_store.as_ref(),
)
.await;
codex_guardian_context::prepare_sync_delivery(&mut context, &history)
let context = PreparedReviewContext::prepare(context, pending.elastic.as_ref(), &history)
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))?;
if context.estimated_tokens().saturating_add(minimum_prefix) > maximum {
let context_tokens = match &context {
PreparedReviewContext::Fixed(context) => context.estimated_tokens(),
PreparedReviewContext::Elastic(context) => context.estimated_tokens(),
};
if context_tokens.saturating_add(minimum_prefix) > maximum {
context
.enforce_budget(
RequestBudget {
max_input_tokens: maximum,
existing_context_tokens: minimum_prefix,
},
GuardianBudgetOmission
.render()
.into_text()
.unwrap_or_default(),
HistoryTruncation::Allow,
)
.map_err(|_error| {
@@ -131,7 +177,7 @@ pub(crate) async fn finalize(
"Guardian expects one review input".to_owned(),
));
};
let mut context = pending.context.clone();
let context = pending.context.clone();
let model = &step.settings.model_info;
let history = session.clone_history().await;
let recovery = session
@@ -155,7 +201,7 @@ pub(crate) async fn finalize(
.await;
// Use the history that will actually reach the model. Recompute after every
// compaction retry; evidence removed by compaction must be delivered again.
codex_guardian_context::prepare_sync_delivery(&mut context, &history)
let context = PreparedReviewContext::prepare(context, pending.elastic.as_ref(), &history)
.map_err(|error| CodexErr::InvalidRequest(error.to_string()))?;
let prompt = build_prompt(
history
@@ -234,14 +280,7 @@ pub(crate) async fn finalize(
history_truncation
};
let mut context = context
.enforce_budget(
budget,
GuardianBudgetOmission
.render()
.into_text()
.unwrap_or_default(),
history_truncation,
)
.enforce_budget(budget, history_truncation)
.map_err(|error| {
session
.services
@@ -364,6 +403,7 @@ pub(crate) async fn finalize(
}
step.turn.extension_data.insert(RetainedReviewContext {
context: pending.context.retained_instructions(),
elastic: pending.elastic.clone(),
history_version,
});
session
@@ -1,6 +1,7 @@
//! Exercises reviewer history safety at cancellation and budget boundaries.
use super::*;
use codex_guardian_context::ContextBudgeting;
use codex_guardian_context::ContextPresentation;
use codex_guardian_context::ContextProfile;
use codex_guardian_context::PlannedAction;
@@ -48,6 +49,7 @@ async fn finalization_exports_loss_totals_only_for_accepted_input(window: i64) {
context.truncations = [(256, 100), (64, 40), (16, 0)]
.into_iter()
.map(|(original_bytes, retained_bytes)| TruncationObservation {
source: None,
component: "conversation_transcript",
original_bytes,
retained_bytes,
@@ -61,7 +63,10 @@ async fn finalization_exports_loss_totals_only_for_accepted_input(window: i64) {
session
.services
.thread_extension_data
.insert(PendingReviewContext { context });
.insert(PendingReviewContext {
context,
elastic: None,
});
assert_eq!(
finalize(&session, &step, &mut input, HistoryTruncation::Preserve)
.await
@@ -177,7 +182,10 @@ async fn cancelled_startup_does_not_record_unselected_review_evidence() {
session
.services
.thread_extension_data
.insert(PendingReviewContext { context });
.insert(PendingReviewContext {
context,
elastic: None,
});
session
.set_session_startup_prewarm(
crate::session::startup_prewarm::SessionStartupPrewarmHandle::new(
@@ -234,7 +242,10 @@ async fn feasibility_rejects_required_evidence_inside_the_reserved_margin() {
session
.services
.thread_extension_data
.insert(PendingReviewContext { context });
.insert(PendingReviewContext {
context,
elastic: None,
});
assert!(check_pending(&session, &turn).await.is_err());
}
@@ -257,7 +268,10 @@ async fn finalization_overflow_marks_the_reviewer_exhausted() {
session
.services
.thread_extension_data
.insert(PendingReviewContext { context });
.insert(PendingReviewContext {
context,
elastic: None,
});
assert!(
finalize(&session, &step, &mut input, HistoryTruncation::Preserve)
@@ -274,7 +288,16 @@ async fn finalization_overflow_marks_the_reviewer_exhausted() {
}
#[tokio::test]
async fn compacted_review_restores_originals_once_and_persists_the_request_prefix() {
#[test_case(ContextBudgeting::Fixed; "fixed")]
#[test_case(ContextBudgeting::Elastic; "elastic")]
async fn compacted_review_restores_originals_once_and_persists_the_request_prefix(
budgeting: ContextBudgeting,
) {
const LARGE_INSTRUCTION_REPEATS: usize = 224;
let instruction = "Do not publish. ".repeat(match budgeting {
ContextBudgeting::Fixed => 1,
ContextBudgeting::Elastic => LARGE_INSTRUCTION_REPEATS,
});
let (session, turn) = crate::session::tests::make_session_and_context().await;
let session = Arc::new(session);
let step = session
@@ -286,7 +309,7 @@ async fn compacted_review_restores_originals_once_and_persists_the_request_prefi
&step.turn,
&step.settings.model_info,
&[codex_protocol::user_input::UserInput::Text {
text: "Do not publish.".to_owned(),
text: instruction.clone(),
text_elements: vec![],
}],
/*client_id*/ None,
@@ -297,9 +320,16 @@ async fn compacted_review_restores_originals_once_and_persists_the_request_prefi
codex_thread_store::PersistContext::Standard,
)
.await;
let context = super::super::prompt::build_guardian_prompt_items(
let collected = super::super::prompt::collect_guardian_prompt_items_with_parent_turn(
&session,
/*retry_reason*/ None,
super::super::prompt::GuardianTranscriptHistory::Retained(
session.conversation_history_snapshot().await.as_ref(),
),
/*parent_context*/ None,
super::super::ApprovalRequestReasons {
approval: None,
retry: None,
},
super::super::GuardianApprovalRequest::RequestPermissions {
id: "review".to_owned(),
environment_id: "local".to_owned(),
@@ -308,12 +338,45 @@ async fn compacted_review_restores_originals_once_and_persists_the_request_prefi
permissions: Default::default(),
},
super::super::prompt::GuardianPromptMode::Full,
/*reviewed_node_repl_evidence_sequence*/ 0,
)
.await
.unwrap()
.context;
.unwrap();
let context = codex_guardian_context::prepare_sync_context(
collected.sections,
&collected.profile,
/*entry_number_offset*/ 0,
ContextPresentation::SyncFull {
session_id: &collected.session_id,
},
budgeting,
)
.unwrap();
let elastic = (budgeting == ContextBudgeting::Elastic).then(|| {
let section = codex_guardian_context::ElasticSection::TrustedSkills;
let mut policy = codex_guardian_context::ElasticBudgetConfig::default();
policy.sections.insert(
section,
codex_guardian_context::ElasticSectionLimits::new(
/*min_total_section_tokens*/ 1, /*max_section_entry_tokens*/ 1,
)
.unwrap(),
);
let minimum = codex_guardian_context::DEFAULT_MAX_INPUT_TOKENS
- usize::try_from(policy.total_minimum_tokens()).unwrap()
+ 1;
policy.sections.insert(
section,
codex_guardian_context::ElasticSectionLimits::new(
minimum, /*max_section_entry_tokens*/ 1,
)
.unwrap(),
);
policy
});
step.turn.extension_data.insert(RetainedReviewContext {
context: context.retained_instructions(),
elastic,
history_version: session.clone_history().await.history_version(),
});
// Simulate a reviewer compaction that discarded the earlier user-message block.
@@ -332,10 +395,11 @@ async fn compacted_review_restores_originals_once_and_persists_the_request_prefi
super::super::request_budget::prepare_prompt(&session, &mut prompt, &step, &metadata)
.await
.unwrap();
assert!(
serde_json::to_string(&prompt.input)
.unwrap()
.contains("Do not publish.")
let restored = serde_json::to_string(&prompt.input).unwrap();
assert!(restored.contains("Do not publish."));
assert_eq!(
restored.contains("<truncated"),
budgeting == ContextBudgeting::Elastic
);
let persisted = session
.clone_history()
+17 -8
View File
@@ -1,8 +1,10 @@
//! Restores compacted retained evidence and checks complete synchronous requests.
//! Restored records are budgeted and persisted before sampling, including continuations.
//! Recovery uses the section policy captured with those records.
//! Admitted sampling inputs are observed once per step, independent of transport retries.
use codex_api::ResponsesApiRequest;
use codex_context_fragments::AnnotatedContent;
use codex_context_fragments::set_annotated_content;
use codex_guardian_context::HistoryTruncation;
use codex_guardian_context::REQUEST_TOKENS_BOUNDARIES;
@@ -12,12 +14,14 @@ use codex_guardian_context::effective_input_token_limit;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ContentItemKind;
use codex_protocol::models::ContentItemMetadata;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::TruncationPolicy;
use super::input_budget::PreparedReviewContext;
use super::input_budget::RetainedReviewContext;
use crate::context::ContextualUserFragment;
use crate::context::GuardianBudgetOmission;
use crate::context::GuardianRetainedInstructions;
use crate::context_manager::estimate_item_token_count;
use crate::session::session::Session;
@@ -78,9 +82,9 @@ pub(crate) async fn prepare_prompt(
metadata,
/*include_internal*/ true,
)?;
let mut context = retained.context.clone();
codex_guardian_context::prepare_sync_delivery(
&mut context,
let context = PreparedReviewContext::prepare(
retained.context.clone(),
retained.elastic.as_ref(),
&history
.for_prompt_annotated(
&model.input_modalities,
@@ -95,10 +99,6 @@ pub(crate) async fn prepare_prompt(
max_input_tokens: maximum,
existing_context_tokens: estimate_request_tokens(&baseline),
},
GuardianBudgetOmission
.render()
.into_text()
.unwrap_or_default(),
HistoryTruncation::Preserve,
)
.map_err(|error| {
@@ -129,6 +129,14 @@ pub(crate) async fn prepare_prompt(
"expected restored Guardian text".to_owned(),
));
};
// Elastic text has already passed allocation and delivery chunking.
if retained.elastic.is_some() {
return Ok(AnnotatedContent::text(
text,
ContentItemKind(GuardianRetainedInstructions::KIND.to_owned()),
ContentItemMetadata::harness(),
));
}
Ok(GuardianRetainedInstructions::try_from(text)?
.render_fragment()
.into_parts()
@@ -175,6 +183,7 @@ pub(crate) async fn prepare_prompt(
{
step.turn.extension_data.insert(RetainedReviewContext {
context: retained.context.clone(),
elastic: retained.elastic.clone(),
history_version,
});
}
@@ -557,6 +557,7 @@ async fn run_review_on_session(
.thread_extension_data
.insert(super::input_budget::PendingReviewContext {
context: prompt_items.context,
elastic: None,
});
review_session
.session
+17 -2
View File
@@ -82,15 +82,28 @@ pub(crate) fn allocation_section(
pub struct PreparedElasticContext<'a> {
pub(crate) context: ComposedContext,
pub(crate) config: &'a ElasticBudgetConfig,
pub(crate) delivered_omissions: Option<codex_history::GuardianRetainedOmissions>,
}
impl PreparedElasticContext<'_> {
/// Deduplicates using the completeness of the capped representations.
/// Removes history-delivered instructions and deduplicates native deliveries.
/// Pending instruction proof is reconciled when the budget is enforced.
pub fn prepare_sync_delivery(
mut self,
history: &[ResponseItemEnvelope],
) -> Result<Self, SectionError> {
crate::prepare_sync_delivery(&mut self.context, history)?;
self.context.remove_delivered_instructions(
history
.iter()
.filter_map(|item| item.metadata.as_ref())
.flat_map(|metadata| &metadata.guardian_sources),
crate::ContextBudgeting::Elastic,
);
self.context.retain_new_native_messages(history)?;
self.delivered_omissions = history
.iter()
.rev()
.find_map(|item| item.metadata.as_ref()?.guardian_retained_omissions);
Ok(self)
}
@@ -115,6 +128,7 @@ impl PreparedElasticContext<'_> {
.ok_or(SectionError::EvidenceLimitExceeded {
section: "request_budget",
})?;
context.retain_selected_instructions(self.delivered_omissions);
let demand = collect_demand(&context)?;
let context_tokens = context.estimated_tokens();
if context_tokens <= remaining && context.truncations.is_empty() {
@@ -311,6 +325,7 @@ fn retain_selected(context: &mut ComposedContext, selected: &HashSet<(usize, usi
&& !selected.contains(&(section_index, 0)) =>
{
context.truncations.push(TruncationObservation {
source: None,
component: section.id,
original_bytes: ByteCount::item(&message.content.item),
retained_bytes: 0,
@@ -118,6 +118,7 @@ fn whole_images_use_defaults_and_charge_shared_framing_once() {
let input = PreparedElasticContext {
context: context.clone(),
config: &config,
delivered_omissions: None,
};
let mut budget = RequestBudget {
max_input_tokens: context.estimated_tokens() + HISTORY_TOKENS,
@@ -438,6 +438,7 @@ impl CollectedContext {
ContextSection::TranscriptImages(images) => {
if images.omitted_bytes > 0 {
truncations.push(TruncationObservation {
source: None,
component: "transcript_image",
original_bytes: images.omitted_bytes,
retained_bytes: 0,
+31 -44
View File
@@ -3,7 +3,7 @@
//! hosts may shorten historical instructions after compaction cannot make room.
//! Every reduction reserves an omission notice and preserves source order.
//! JSON recovery shortens only the body; line mode shortens the whole rendered entry.
//! Native messages and tool calls are reconciled with current history before budgeting.
//! Native deduplication prefers required and protected copies before optional eviction.
use std::collections::HashMap;
use std::collections::HashSet;
@@ -13,7 +13,6 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::ImageReference;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::TruncationPolicy;
use crate::ComposedContext;
use crate::RequestBudget;
@@ -25,6 +24,9 @@ use crate::budget::section_tokens;
use crate::composition::SectionContent;
use crate::composition::SectionDelivery;
use crate::composition::SectionOutput;
use crate::truncation::truncate_content_to_fit;
const MIN_HISTORICAL_TEXT_TOKENS: usize = 32;
/// Eviction priority within a profile; older items at the same priority go first.
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
@@ -120,8 +122,9 @@ pub enum HistoryTruncation {
impl ComposedContext {
/// Omits native messages and tool calls already present in the effective reviewer history
/// or earlier in this input. Hosts must repeat this check after compaction;
/// missing evidence may be delivered again. Identity excludes host annotations.
/// or another pending copy, preferring required and protected copies.
/// Hosts repeat this check after compaction; missing evidence may be delivered again.
/// Identity excludes host annotations.
/// Conflicting payloads under one ID reject the input without changing it.
pub fn retain_new_native_messages(
&mut self,
@@ -140,14 +143,24 @@ impl ComposedContext {
let mut seen_messages = HashMap::new();
let mut seen_calls = HashMap::new();
let mut duplicates = HashSet::new();
let pending =
self.sections
.iter()
.enumerate()
.filter_map(|(index, section)| match &section.delivery {
SectionDelivery::Message(message) => Some((Some(index), &message.content.item)),
SectionDelivery::UserContent(_) => None,
});
let mut pending = self
.sections
.iter()
.enumerate()
.filter_map(|(index, section)| match &section.delivery {
SectionDelivery::Message(message) => Some((index, message)),
SectionDelivery::UserContent(_) => None,
})
.collect::<Vec<_>>();
pending.sort_by_key(|(_, message)| {
(
matches!(message.retention, Retention::Optional(_)),
!message.protected,
)
});
let pending = pending
.into_iter()
.map(|(index, message)| (Some(index), &message.content.item));
for (index, item) in reviewer_history
.iter()
.map(|item| (None, &item.item))
@@ -299,47 +312,19 @@ impl ComposedContext {
continue;
}
let original_tokens = section_content_tokens(&item.content);
let original_bytes = match &item.content {
SectionContent::Transcript(record) => record.rendered().text_bytes,
SectionContent::Other(ContentItem::InputText { text }) => text.len(),
_ => continue,
};
let Some(original_body) = item.content.replace_text(String::new()) else {
continue;
};
let original_bytes = item.content.evidence_bytes();
let target = original_tokens.saturating_sub(required_tokens - remaining);
let mut upper = TruncationPolicy::Bytes(original_body.len()).token_budget();
let mut lower = 32.min(upper);
let mut selected = lower;
while lower < upper {
let mid = lower + (upper - lower).div_ceil(2);
let _ = item
.content
.replace_text(crate::truncate_text(&original_body, mid));
if section_content_tokens(&item.content) <= target {
lower = mid;
selected = mid;
} else {
upper = mid - 1;
}
}
let _ = item
.content
.replace_text(crate::truncate_text(&original_body, selected));
truncate_content_to_fit(&mut item.content, target, MIN_HISTORICAL_TEXT_TOKENS);
let saved =
original_tokens.saturating_sub(section_content_tokens(&item.content));
if saved == 0 {
let _ = item.content.replace_text(original_body);
continue;
}
self.truncations.push(TruncationObservation {
source: None,
component: section.id,
original_bytes,
retained_bytes: match &item.content {
SectionContent::Transcript(record) => record.rendered().text_bytes,
SectionContent::Other(ContentItem::InputText { text }) => text.len(),
_ => 0,
},
retained_bytes: item.content.evidence_bytes(),
});
history_truncated = true;
required_tokens = required_tokens.saturating_sub(saved);
@@ -398,6 +383,7 @@ impl ComposedContext {
&& removed.contains(&(section_index, 0))
{
self.truncations.push(TruncationObservation {
source: None,
component: section.id,
original_bytes: serde_json::to_vec(&message.content.item)
.map_or(usize::MAX, |bytes| bytes.len()),
@@ -434,6 +420,7 @@ pub(crate) fn retain_content(
if !keep {
let original_bytes = item.content.evidence_bytes();
truncations.push(TruncationObservation {
source: None,
component: section.id,
original_bytes,
retained_bytes: 0,
@@ -87,6 +87,66 @@ fn native_agent_admission_uses_current_history_and_preserves_identity() {
assert_eq!(admitted.into_messages(), expected);
}
// Both budgeting modes keep a Required or protected call rather than its
// earlier Optional duplicate, which can be evicted under budget pressure.
let exact: ResponseItem = serde_json::from_value(serde_json::json!({
"type": "function_call", "call_id": "exact", "name": "exec_command", "arguments": "{}"
}))
.unwrap();
for action in [
Budgeted::required(Box::new(exact.clone().into())),
Budgeted::protected(Box::new(exact.clone().into())),
] {
let pending = ComposedContext {
sections: vec![
SectionOutput {
id: "conversation_transcript",
delivery: SectionDelivery::Message(Budgeted::optional(
Box::new(exact.clone().into()),
BudgetPriority::Tool,
)),
},
SectionOutput {
id: "planned_action",
delivery: SectionDelivery::Message(action),
},
],
truncations: Vec::new(),
};
let exact_budget = section_tokens(&pending.sections[1]);
let mut expected = pending.clone();
expected.sections.remove(/*index*/ 0);
let mut fixed = pending.clone();
crate::prepare_sync_delivery(&mut fixed, &[]).unwrap();
let fixed = fixed
.enforce_budget(
RequestBudget {
max_input_tokens: exact_budget,
existing_context_tokens: 0,
},
"evidence omitted".to_owned(),
HistoryTruncation::Preserve,
)
.unwrap();
assert_eq!(fixed.sections, expected.sections);
let config =
crate::budget::tests::complete_minimum_budget(crate::ElasticBudgetConfig::default());
let admitted = pending
.prepare_elastic_entries(&config)
.unwrap()
.prepare_sync_delivery(&[])
.unwrap()
.enforce_budget(
RequestBudget {
max_input_tokens: exact_budget,
existing_context_tokens: 0,
},
"evidence omitted".to_owned(),
)
.unwrap();
assert_eq!(admitted.sections, expected.sections);
}
let mut conflicting = context;
let SectionDelivery::Message(message) = &mut conflicting.sections[1].delivery else {
panic!("native message")
@@ -68,6 +68,7 @@ impl ComposedContext {
source.complete = false;
}
self.truncations.push(TruncationObservation {
source: item.source.clone(),
component: section.id,
original_bytes,
retained_bytes: item.content.evidence_bytes(),
@@ -79,6 +80,7 @@ impl ComposedContext {
Ok(PreparedElasticContext {
context: self,
config,
delivered_omissions: None,
})
}
}
@@ -286,6 +286,7 @@ impl ContextProfile {
};
if self.target == ContextTarget::Async && entry.original_bytes > retained_bytes {
truncations.push(TruncationObservation {
source: None,
component,
original_bytes: entry.original_bytes,
retained_bytes,
@@ -250,17 +250,39 @@ impl ComposedContext {
}
}
/// Omit complete source revisions still present in the admitted reviewer history.
/// Omit complete source revisions in admitted history or required pending transcript entries.
/// Missing host metadata, changed revisions and incomplete copies require redelivery.
pub fn retain_new_instructions(&mut self, reviewer_history: &[ResponseItemEnvelope]) {
self.remove_delivered_instructions(reviewer_history);
// Read only section-scoped delivery proof. Coalesced tool output can contain
// identical notice text without establishing that the host delivered it.
let Some(previous) = reviewer_history
let transcript_sources = self.required_transcript_sources();
self.remove_delivered_instructions(
reviewer_history
.iter()
.filter_map(|item| item.metadata.as_ref())
.flat_map(|metadata| &metadata.guardian_sources)
.chain(&transcript_sources),
crate::ContextBudgeting::Fixed,
);
let previous = reviewer_history
.iter()
.rev()
.find_map(|item| item.metadata.as_ref()?.guardian_retained_omissions)
else {
.find_map(|item| item.metadata.as_ref()?.guardian_retained_omissions);
self.prune_omission_notices(previous);
}
pub(crate) fn retain_selected_instructions(
&mut self,
previous: Option<GuardianRetainedOmissions>,
) {
let transcript_sources = self.required_transcript_sources();
self.remove_delivered_instructions(
transcript_sources.iter(),
crate::ContextBudgeting::Elastic,
);
self.prune_omission_notices(previous);
}
fn prune_omission_notices(&mut self, previous: Option<GuardianRetainedOmissions>) {
let Some(previous) = previous else {
return;
};
self.sections.retain_mut(|section| {
@@ -290,19 +312,28 @@ impl ComposedContext {
});
}
fn remove_delivered_instructions(&mut self, reviewer_history: &[ResponseItemEnvelope]) {
let transcript_sources: HashSet<_> = self
.sections
fn required_transcript_sources(&self) -> Vec<codex_history::RetainedSource> {
self.sections
.iter()
.filter(|section| section.id == "conversation_transcript")
.flat_map(|section| match &section.delivery {
SectionDelivery::UserContent(items) => items.as_slice(),
SectionDelivery::Message(_) => &[],
})
.filter(|item| item.retention == crate::Retention::Required)
.filter_map(|item| item.source.as_ref())
.filter(|source| source.complete)
.cloned()
.collect();
.collect()
}
/// Sources must come from current reviewer history or required entries that survived selection.
pub(crate) fn remove_delivered_instructions<'a>(
&mut self,
delivered_sources: impl Iterator<Item = &'a codex_history::RetainedSource> + Clone,
budgeting: crate::ContextBudgeting,
) {
let truncations = &mut self.truncations;
for section in self
.sections
.iter_mut()
@@ -313,14 +344,27 @@ impl ComposedContext {
};
// At most sixteen retained originals are checked; no history-sized index is needed.
items.retain(|item| {
item.source.as_ref().is_none_or(|source| {
!reviewer_history
.iter()
.filter_map(|item| item.metadata.as_ref())
.flat_map(|metadata| &metadata.guardian_sources)
.chain(&transcript_sources)
.any(|delivered| delivered.complete && delivered == source)
})
let keep = item.source.as_ref().is_none_or(|source| {
!delivered_sources.clone().any(|delivered| {
delivered.complete
&& match budgeting {
crate::ContextBudgeting::Fixed => delivered == source,
// The candidate may have been capped; only the surviving
// delivered copy must still be complete.
crate::ContextBudgeting::Elastic => {
delivered.id == source.id
&& delivered.revision == source.revision
}
}
})
});
if !keep && budgeting == crate::ContextBudgeting::Elastic {
truncations.retain(|observation| {
observation.component != "retained_user_instructions"
|| observation.source.as_ref() != item.source.as_ref()
});
}
keep
});
}
}
@@ -294,13 +294,15 @@ fn legacy_verified_answers_keep_distinct_source_order() {
#[test]
fn delivery_uses_source_revision_and_complete_host_metadata() {
const REPEATS: usize = 64;
const ENTRY_TOKENS: usize = 128;
let mut retained = RetainedContext::default();
retained.mark_user_messages_incomplete();
let mut message = RetainedUserMessage {
phase: None,
turn_id: "turn".to_owned(),
message_id: Some("source".to_owned()),
text: "Draft only.".to_owned(),
text: "Draft only. ".repeat(REPEATS),
complete: true,
origin: codex_history::UserInputOrigin::User,
};
@@ -321,6 +323,32 @@ fn delivery_uses_source_revision_and_complete_host_metadata() {
next.retain_new_instructions(&delivered);
assert!(next.into_messages().is_empty());
// A complete admitted revision also covers a newly capped duplicate.
let config = crate::budget::tests::complete_minimum_budget(crate::ElasticBudgetConfig {
sections: std::collections::BTreeMap::from([(
crate::ElasticSection::RetainedUserInstructions,
crate::ElasticSectionLimits::new(ENTRY_TOKENS, ENTRY_TOKENS).unwrap(),
)]),
});
let capped = original.clone().prepare_elastic_entries(&config).unwrap();
assert!(!capped.context.truncations.is_empty());
let history_tokens = original.estimated_tokens();
assert!(
capped
.prepare_sync_delivery(&delivered)
.unwrap()
.enforce_budget(
crate::RequestBudget {
max_input_tokens: history_tokens,
existing_context_tokens: history_tokens,
},
"omitted".to_owned()
)
.unwrap()
.into_messages()
.is_empty()
);
// Identical prompt text alone, or a shortened copy with the same source ID, is not proof.
for metadata in [
None,
+1
View File
@@ -166,6 +166,7 @@ pub fn render_review_evidence(review: ReviewEvidence<'_>) -> RenderedReviewEvide
let text = truncate_entry(&text, token_cap);
if original_bytes > text.len() {
truncations.push(TruncationObservation {
source: None,
component,
original_bytes,
retained_bytes: text.len(),
@@ -81,6 +81,8 @@ mod tests;
/// Actual evidence reduction, reported by consumers using their existing metrics.
#[derive(Clone)]
pub struct TruncationObservation {
/// Owning source revision, so deduplicated copies do not report evidence loss.
pub source: Option<codex_history::RetainedSource>,
pub component: &'static str,
pub original_bytes: usize,
pub retained_bytes: usize,