Retain user instructions in guardian thread context (#42762)

## Why

Guardian review needs the original user instructions alongside verified answers, even after compaction, resume, or rollback. A summarized or legacy checkpoint cannot establish a complete instruction history.

## What changed

- Capture text user messages in retained context when `guardian_thread_context` is enabled, preserving their message identity through compaction and replay.
- Keep retained instructions and verified answers in arrival order, with independent size and completeness tracking for each family.
- Roll back retained evidence at the exact user-message boundary, including steers that share a turn ID, and apply the same semantics during rollout migration.
- Treat checkpoints without retained user messages as incomplete while preserving their verified answers.

## Testing

- Cover capture, compaction, resume, rollback, child forks, legacy checkpoints, storage bounds, and rollout migration.

GitOrigin-RevId: 72706731b54e5ac8645db61b446d2401afb42235
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-09-04 13:24:42 +00:00
1 parent cc4b8bdeb8
commit 80d7ca34bc
18 files changed
+1058 -103

No files matched your search

+26 -2
View File
@@ -32,8 +32,10 @@ use codex_analytics::CompactionTrigger;
use codex_analytics::now_unix_seconds;
use codex_context_fragments::AnnotatedContent;
use codex_context_fragments::set_annotated_content;
use codex_features::Feature;
use codex_history::CodexHarnessMetadata;
use codex_history::ResponseItemEnvelope;
use codex_protocol::ResponseItemId;
use codex_protocol::error::CodexErr;
use codex_protocol::error::CodexErrorDetails;
use codex_protocol::error::Result as CodexResult;
@@ -355,7 +357,12 @@ async fn run_compact_task_inner_impl(
let summary_suffix =
get_last_assistant_message_from_turn(history_snapshot.raw_items()).unwrap_or_default();
let summary_text = format!("{SUMMARY_PREFIX}\n{summary_suffix}");
let user_messages = collect_annotated_user_messages(history_items);
let identity = if sess.enabled(Feature::GuardianThreadContext) {
CompactedMessageIdentity::Preserve
} else {
CompactedMessageIdentity::Regenerate
};
let user_messages = collect_annotated_user_messages(history_items, identity);
let mut new_history = build_compacted_history(Vec::new(), &user_messages, &summary_text);
if let Some(summary_item) = new_history.last_mut() {
@@ -527,6 +534,9 @@ pub fn content_items_to_text(content: &[ContentItem]) -> Option<String> {
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct CompactedUserMessage {
// Keep source identity even when compaction shortens the text, so rollback can
// correlate the rebuilt message with thread-owned retained evidence.
id: Option<ResponseItemId>,
message: String,
internal_chat_message_metadata_passthrough: Option<InternalChatMessageMetadataPassthrough>,
harness_metadata: Option<CodexHarnessMetadata>,
@@ -540,12 +550,24 @@ pub(crate) fn collect_user_messages(items: &[ResponseItem]) -> Vec<CompactedUser
.collect()
}
pub(crate) enum CompactedMessageIdentity {
Preserve,
Regenerate,
}
pub(crate) fn collect_annotated_user_messages(
items: &[ResponseItemEnvelope],
identity: CompactedMessageIdentity,
) -> Vec<CompactedUserMessage> {
items
.iter()
.filter_map(|envelope| compacted_user_message(&envelope.item, envelope.metadata.clone()))
.map(|mut message| {
if matches!(identity, CompactedMessageIdentity::Regenerate) {
message.id = None;
}
message
})
.collect()
}
@@ -560,6 +582,7 @@ fn compacted_user_message(
return None;
}
Some(CompactedUserMessage {
id: item.id().cloned(),
message: user.message(),
internal_chat_message_metadata_passthrough: match item {
ResponseItem::Message {
@@ -678,6 +701,7 @@ fn build_compacted_history_with_limit(
let truncated =
truncate_text(&message.message, TruncationPolicy::Tokens(remaining));
selected_messages.push(CompactedUserMessage {
id: message.id.clone(),
message: truncated,
internal_chat_message_metadata_passthrough: message
.internal_chat_message_metadata_passthrough
@@ -692,7 +716,7 @@ fn build_compacted_history_with_limit(
for message in &selected_messages {
let mut item = ResponseItem::Message {
id: None,
id: message.id.clone(),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: message.message.clone(),