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

+14 -3
View File
@@ -66,6 +66,7 @@ fn user_message(text: &str) -> ResponseItem {
fn compacted_user_message(text: &str) -> CompactedUserMessage {
CompactedUserMessage {
id: None,
message: text.to_string(),
internal_chat_message_metadata_passthrough: None,
harness_metadata: None,
@@ -129,7 +130,13 @@ fn collect_user_messages_extracts_user_text_only() {
let collected = collect_user_messages(&items);
assert_eq!(vec![compacted_user_message("first")], collected);
assert_eq!(
vec![CompactedUserMessage {
id: Some(ResponseItemId::with_suffix("msg", "user")),
..compacted_user_message("first")
}],
collected,
);
}
#[test]
@@ -142,10 +149,11 @@ fn collect_annotated_user_messages_extracts_user_text_only() {
ResponseItemEnvelope::new(ResponseItem::Other),
];
let collected = collect_annotated_user_messages(&items);
let collected = collect_annotated_user_messages(&items, CompactedMessageIdentity::Preserve);
assert_eq!(
vec![CompactedUserMessage {
id: None,
message: "first".to_string(),
internal_chat_message_metadata_passthrough: None,
harness_metadata: Some(CodexHarnessMetadata::default()),
@@ -223,6 +231,7 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
let max_tokens = 16;
let big = "word ".repeat(200);
let user_message = CompactedUserMessage {
id: Some(ResponseItemId::with_suffix("msg", "long-user")),
message: big.clone(),
internal_chat_message_metadata_passthrough: None,
harness_metadata: Some(CodexHarnessMetadata::default()),
@@ -261,6 +270,7 @@ fn build_token_limited_compacted_history_truncates_overlong_user_messages() {
other => panic!("unexpected item in history: {other:?}"),
};
assert_eq!(summary_text, "SUMMARY");
assert_eq!(history[0].id(), user_message.id.as_ref());
assert_eq!(history[0].metadata, Some(CodexHarnessMetadata::default()));
assert_eq!(history[1].metadata, None);
}
@@ -292,6 +302,7 @@ fn build_compacted_history_preserves_user_message_passthrough_metadata() {
let history = build_compacted_history(
Vec::new(),
&[CompactedUserMessage {
id: Some(ResponseItemId::with_suffix("msg", "user")),
message: "first user message".to_string(),
internal_chat_message_metadata_passthrough: Some(
InternalChatMessageMetadataPassthrough {
@@ -314,7 +325,7 @@ fn build_compacted_history_preserves_user_message_passthrough_metadata() {
vec![
ResponseItemEnvelope {
item: ResponseItem::Message {
id: None,
id: Some(ResponseItemId::with_suffix("msg", "user")),
role: "user".to_string(),
content: vec![ContentItem::InputText {
text: "first user message".to_string(),