mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Preserve encrypted agent messages in Guardian reviews (#49038)
## Why Guardian transcripts omitted encrypted agent messages, leaving reviews without evidence such as encrypted parent replies. ## What changed - Carry encrypted `agent_message` items through synchronous reviews and asynchronous scoring, preserving their order, author, recipient, and ciphertext. - Account for native messages in context budgets and omit them whole with an explicit notice when limits require it. - Support mixed text and native messages during review admission while retaining user-content annotations. ## Testing Add coverage for transcript ordering and budget eviction in both reviewer profiles, plus a mocked scenario verifying that an encrypted parent reply appears exactly once in each of two incremental Guardian reviews. GitOrigin-RevId: abc2f1ce5accd83beb06f799ef9ed9d7fa2cd90a
This commit is contained in:
@@ -66,29 +66,32 @@ fn heartbeat_references_preserve_positions_changes_and_human_messages() {
|
||||
);
|
||||
}
|
||||
for index in [1, 3] {
|
||||
assert!(
|
||||
entries[index]
|
||||
.text
|
||||
.contains("unchanged from transcript entry [1]")
|
||||
);
|
||||
assert!(!entries[index].text.contains("Monitor only."));
|
||||
let crate::TranscriptContent::Text(text) = &entries[index].content else {
|
||||
panic!("text entry")
|
||||
};
|
||||
assert!(text.contains("unchanged from transcript entry [1]"));
|
||||
assert!(!text.contains("Monitor only."));
|
||||
}
|
||||
// A rebuilt window starts with a full body, never a dangling old reference.
|
||||
let rebuilt = collect_transcript(&history[3..].to_vec(), &transcript_config());
|
||||
assert!(rebuilt[0].text.contains("Monitor only."));
|
||||
assert!(
|
||||
matches!(&rebuilt[0].content, crate::TranscriptContent::Text(text) if text.contains("Monitor only."))
|
||||
);
|
||||
let interrupted = vec![
|
||||
history[0].clone(),
|
||||
make("02:00Z", &"x".repeat(/*n*/ 3_600), "user.heartbeat"),
|
||||
history[1].clone(),
|
||||
];
|
||||
let interrupted = collect_transcript(&interrupted, &transcript_config());
|
||||
assert!(interrupted[2].text.contains("Monitor only."));
|
||||
assert!(
|
||||
matches!(&interrupted[2].content, crate::TranscriptContent::Text(text) if text.contains("Monitor only."))
|
||||
);
|
||||
}
|
||||
|
||||
fn entry(kind: ConversationTranscriptEntryKind, text: &str) -> ConversationTranscriptEntry {
|
||||
ConversationTranscriptEntry {
|
||||
kind,
|
||||
text: text.to_string(),
|
||||
content: crate::TranscriptContent::Text(text.to_string()),
|
||||
original_bytes: text.len(),
|
||||
retained_source: None,
|
||||
}
|
||||
@@ -428,7 +431,7 @@ fn reused_registry_applies_current_history_sources_and_entry_limits() {
|
||||
transcript_items(§ions[0]),
|
||||
vec![ConversationTranscriptEntry {
|
||||
kind: ConversationTranscriptEntryKind::User,
|
||||
text: text.clone(),
|
||||
content: crate::TranscriptContent::Text(text.clone()),
|
||||
original_bytes: text.len(),
|
||||
retained_source: None,
|
||||
}]
|
||||
@@ -466,7 +469,7 @@ fn reused_registry_applies_current_history_sources_and_entry_limits() {
|
||||
.expect("transcript collection should succeed");
|
||||
let mut expected = vec![ConversationTranscriptEntry {
|
||||
kind: ConversationTranscriptEntryKind::User,
|
||||
text: text.clone(),
|
||||
content: crate::TranscriptContent::Text(text.clone()),
|
||||
original_bytes: text.len(),
|
||||
retained_source: None,
|
||||
}];
|
||||
@@ -475,7 +478,9 @@ fn reused_registry_applies_current_history_sources_and_entry_limits() {
|
||||
kind: ConversationTranscriptEntryKind::ToolCall(
|
||||
"tool exec_command call".to_string(),
|
||||
),
|
||||
text: truncate_text(&text, /*max_tokens*/ 30),
|
||||
content: crate::TranscriptContent::Text(truncate_text(
|
||||
&text, /*max_tokens*/ 30,
|
||||
)),
|
||||
original_bytes: text.len(),
|
||||
retained_source: None,
|
||||
});
|
||||
@@ -490,3 +495,81 @@ fn transcript_items(section: &ContextSection) -> &[ConversationTranscriptEntry]
|
||||
};
|
||||
items
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_messages_preserve_order_and_budget_for_both_reviewers() {
|
||||
use crate::ContextPresentation;
|
||||
use crate::ContextProfile;
|
||||
use crate::HistoryTruncation;
|
||||
use crate::RequestBudget;
|
||||
use codex_protocol::models::AgentMessageInputContent;
|
||||
let message = ResponseItem::AgentMessage {
|
||||
id: None,
|
||||
author: "/root".into(),
|
||||
recipient: "/root/worker".into(),
|
||||
content: vec![AgentMessageInputContent::EncryptedContent {
|
||||
encrypted_content: "opaque-parent-reply".repeat(/*n*/ 80),
|
||||
}],
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
let user = ResponseItem::Message {
|
||||
id: None,
|
||||
role: "user".into(),
|
||||
content: vec![ContentItem::InputText {
|
||||
text: "Update this page.".into(),
|
||||
}],
|
||||
phase: None,
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
let history = vec![user.clone(), message.clone(), user];
|
||||
for profile in [
|
||||
ContextProfile::synchronous(),
|
||||
ContextProfile::asynchronous(),
|
||||
] {
|
||||
let entries = collect_transcript(&history, &profile.transcript);
|
||||
let context = crate::CollectedContext {
|
||||
sections: vec![ContextSection::ConversationTranscript {
|
||||
items: entries.clone(),
|
||||
}],
|
||||
}
|
||||
.compose(
|
||||
ContextPresentation::SyncFull {
|
||||
session_id: "worker",
|
||||
},
|
||||
profile.render_transcript(&entries, /*entry_number_offset*/ 0),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
context
|
||||
.section_costs()
|
||||
.filter(|(name, _)| *name == "conversation_transcript")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let messages = context.clone().into_messages();
|
||||
assert_eq!(messages.len(), 3);
|
||||
assert_eq!(messages[1], message);
|
||||
// Native messages must be evicted whole, with an explicit omission notice.
|
||||
let budget = context.estimated_tokens() - crate::estimate_input_tokens(&message) + 100;
|
||||
let reduced = context
|
||||
.enforce_budget(
|
||||
RequestBudget {
|
||||
max_input_tokens: budget,
|
||||
existing_context_tokens: 0,
|
||||
},
|
||||
"Evidence omitted.".into(),
|
||||
HistoryTruncation::Preserve,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(reduced.estimated_tokens() <= budget);
|
||||
let reduced = serde_json::to_value(reduced.into_messages()).unwrap();
|
||||
assert!(reduced.to_string().contains("Evidence omitted."));
|
||||
assert!(!reduced.to_string().contains("opaque-parent-reply"));
|
||||
let mut limited = profile.transcript;
|
||||
limited.entry_limits.message_tokens = 10;
|
||||
let entries = collect_transcript(&history, &limited);
|
||||
assert!(
|
||||
matches!(&entries[1].content, crate::TranscriptContent::Text(text) if text.contains("omitted"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user