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:
felixxia-oai
2026-09-04 21:07:14 +00:00
committed by copyberry
parent 8e4b7d31de
commit 4636819a35
15 changed files with 598 additions and 104 deletions
@@ -11,6 +11,7 @@ use crate::compact::is_summary_message;
use crate::context::GuardianReviewEvidence;
use crate::context::is_contextual_user_fragment;
use crate::event_mapping::parse_turn_item;
use crate::guardian::GUARDIAN_MAX_ROOT_MESSAGE_TOKENS;
use crate::guardian::guardian_truncate_text;
use codex_protocol::AgentPath;
use codex_protocol::ThreadId;
@@ -22,7 +23,6 @@ use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::MultiAgentVersion;
const MAX_ROOT_MESSAGES: usize = 8;
const MAX_ROOT_MESSAGE_TOKENS: usize = 900;
impl AgentControl {
/// Returns bounded root conversation and authorization state for a MultiAgent V2 worker.
@@ -57,8 +57,8 @@ impl AgentControl {
.filter_map(|entry| match entry {
codex_history::RetainedContextEntry::UserMessage(message) => {
let text = if message.text.is_empty() && !message.complete {
// Storage may omit a large instruction. After parent compaction,
// Guardian history can still retain that exact source message.
// Older records may omit a large instruction. Recover that exact
// source while it remains available in the parent context.
let original = message.message_id.as_deref().and_then(|id| {
root_history.raw_items().chain(history.review_items()).find(
|item| item.id().is_some_and(|item_id| item_id.as_str() == id),
@@ -84,7 +84,7 @@ impl AgentControl {
.then(|| {
latest_user_turn_id = Some(message.turn_id.clone());
GuardianRootMessage::User(
guardian_truncate_text(&text, MAX_ROOT_MESSAGE_TOKENS).0,
guardian_truncate_text(&text, GUARDIAN_MAX_ROOT_MESSAGE_TOKENS).0,
)
})
}
@@ -113,7 +113,7 @@ impl AgentControl {
})
.collect::<String>();
Some(GuardianRootMessage::Assistant(
guardian_truncate_text(&text, MAX_ROOT_MESSAGE_TOKENS).0,
guardian_truncate_text(&text, GUARDIAN_MAX_ROOT_MESSAGE_TOKENS).0,
))
})
.collect::<Vec<_>>();
@@ -147,7 +147,8 @@ impl AgentControl {
.then(|| {
latest_user_turn_id = item.turn_id().map(str::to_owned);
GuardianRootMessage::User(
guardian_truncate_text(&message, MAX_ROOT_MESSAGE_TOKENS).0,
guardian_truncate_text(&message, GUARDIAN_MAX_ROOT_MESSAGE_TOKENS)
.0,
)
})
}
@@ -162,7 +163,7 @@ impl AgentControl {
})
.collect::<String>();
Some(GuardianRootMessage::Assistant(
guardian_truncate_text(&text, MAX_ROOT_MESSAGE_TOKENS).0,
guardian_truncate_text(&text, GUARDIAN_MAX_ROOT_MESSAGE_TOKENS).0,
))
}
(_, ResponseItem::FunctionCall { call_id, .. }) => root_evidence