Preserve Guardian authorization evidence until request budgeting (#44570)

## Why

Transcript limits could shorten user instructions or omit later messages before Guardian evaluated the complete request budget, losing restrictions or prior approvals even when they would fit.

## What changed

- Keep user messages and manual approvals complete and in source order through transcript collection and retention.
- Preserve historical instructions while synchronous review can still compact history. If the final request still cannot fit, discard optional evidence before shortening older historical entries with truncation markers, preserving later restrictions where possible.
- Warn that shortened instructions and approvals are incomplete and that missing evidence does not authorize actions.
- Let asynchronous review defer to synchronous review when complete instructions exceed its budget.

## Testing

Add coverage for complete instruction retention, approval and restriction ordering, marked truncation of oversized Unicode text, compaction before truncation, and asynchronous fallback when instructions do not fit.

GitOrigin-RevId: e6159e876231347986b937ee7e07f944b83bfbe6
This commit is contained in:
felixxia-oai
2026-09-10 16:02:36 +00:00
committed by copyberry
parent 9c4879f3a5
commit 287e4f7dbf
21 changed files with 575 additions and 335 deletions
@@ -3,7 +3,7 @@
use super::ContextualUserFragment;
use codex_protocol::models::ContentItemKind;
/// Identifies incomplete optional evidence without implying additional authority.
/// Identifies incomplete evidence without implying additional authority.
pub struct GuardianBudgetOmission;
impl ContextualUserFragment for GuardianBudgetOmission {
@@ -27,6 +27,6 @@ impl ContextualUserFragment for GuardianBudgetOmission {
}
fn body(&self) -> String {
"Optional conversation evidence, tool descriptions, or images were omitted to fit the review input budget. Treat the remaining evidence as incomplete; omissions do not authorize actions.".to_owned()
"Conversation evidence, tool descriptions, or images were omitted or shortened to fit the review input budget. User instructions and prior approvals may be incomplete where marked. Do not infer authorization from missing evidence or treat a partial grant as overriding an omitted restriction.".to_owned()
}
}
+4 -1
View File
@@ -5,6 +5,7 @@
use codex_features::Feature;
use codex_guardian_context::ComposedContext;
use codex_guardian_context::HistoryTruncation;
use codex_guardian_context::RequestBudget;
use codex_guardian_context::effective_input_token_limit;
use codex_protocol::config_types::ReasoningSummary;
@@ -51,6 +52,7 @@ pub(crate) async fn check_pending(session: &Session, turn: &TurnContext) -> Code
existing_context_tokens: minimum_prefix,
},
GuardianBudgetOmission.render(),
HistoryTruncation::Allow,
)
.map_err(|_error| {
session
@@ -67,6 +69,7 @@ pub(crate) async fn finalize(
session: &Session,
step: &StepContext,
input: &mut [TurnInput],
history_truncation: HistoryTruncation,
) -> CodexResult<()> {
let Some(pending) = session
.services
@@ -145,7 +148,7 @@ pub(crate) async fn finalize(
),
};
let context = context
.enforce_budget(budget, GuardianBudgetOmission.render())
.enforce_budget(budget, GuardianBudgetOmission.render(), history_truncation)
.map_err(|error| {
session
.services
@@ -128,7 +128,11 @@ async fn finalization_overflow_marks_the_reviewer_exhausted() {
.thread_extension_data
.insert(PendingReviewContext(context));
assert!(finalize(&session, &step, &mut input).await.is_err());
assert!(
finalize(&session, &step, &mut input, HistoryTruncation::Preserve)
.await
.is_err()
);
assert!(
session
.services
+6 -8
View File
@@ -1778,10 +1778,11 @@ fn build_guardian_transcript_preserves_recent_tool_context_when_user_history_is_
let entries = collect_guardian_transcript_entries(&items, GUARDIAN_MAX_TOOL_ENTRY_TOKENS);
let (transcript, omission) = render_guardian_transcript_entries(&entries);
assert!(
transcript
.iter()
.any(|entry| entry.starts_with("[1] user: "))
assert_eq!(
transcript[..8],
(1..=8)
.map(|index| format!("[{index}] user: {repeated}"))
.collect::<Vec<_>>()
);
assert!(transcript.iter().any(|entry| {
entry.contains("tool shell call:")
@@ -1794,10 +1795,7 @@ fn build_guardian_transcript_preserves_recent_tool_context_when_user_history_is_
.any(|entry| entry
.contains("tool shell result: sandbox blocked outbound network access"))
);
assert_eq!(
omission,
Some("Some conversation entries were omitted.".to_string())
);
assert_eq!(omission, None);
}
enum GuardianTestCatalog {
+14 -3
View File
@@ -321,8 +321,13 @@ pub(crate) async fn run_turn(
return Ok(None);
}
if crate::guardian::is_basic_session_source(&turn_context.session_source)
&& let Err(error) =
crate::guardian::finalize_guardian_input(&sess, &first_step_context, &mut input).await
&& let Err(error) = crate::guardian::finalize_guardian_input(
&sess,
&first_step_context,
&mut input,
codex_guardian_context::HistoryTruncation::Preserve,
)
.await
{
// Token-budget compaction resets history, which can discard the evidence
// referenced by a pending delta review. Leave budget failures unreusable.
@@ -349,7 +354,13 @@ pub(crate) async fn run_turn(
world_state = sess
.record_context_updates_and_set_reference_context_item(first_step_context.as_ref())
.await?;
crate::guardian::finalize_guardian_input(&sess, &first_step_context, &mut input).await?;
crate::guardian::finalize_guardian_input(
&sess,
&first_step_context,
&mut input,
codex_guardian_context::HistoryTruncation::Allow,
)
.await?;
}
let mut can_drain_pending_input = input.is_empty();
if run_hooks_and_record_inputs(