Files
codex/codex-rs/history/src/retained_source.rs
T
felixxia-oai 86be5320b0 Deduplicate retained instructions across Guardian reviews (#48060)
## Why

Reused Guardian sessions already contain previously delivered instructions. Follow-up reviews can avoid repeating that evidence, but must deliver it again if it changes or compaction removes it.

## What changed

- Track retained source identity, revision, and completeness in host-owned history metadata, preserving revisions during replay.
- Omit retained instructions from synchronous review inputs only when the same complete revision remains in reviewer history.
- Restore missing evidence after reviewer compaction, enforcing the request budget before persisting additions. Classify restored evidence as context rather than new user authorization.
- Keep source-order labels stable across eviction and retain full delivery for legacy records without reliable provenance.

## Testing

Add coverage for consecutive-review deduplication, changed revisions, missing or incomplete metadata, and restoring compacted evidence exactly once. Extend compaction and resume coverage to verify retained revisions.

GitOrigin-RevId: e90fca16cde0a0970da36dd873af2ca1dbaaf4d3
2026-09-25 10:49:50 +00:00

90 lines
3.2 KiB
Rust

//! Host-owned identity and revision of original evidence. Prompt text cannot create delivery proof.
//! Revisions live with retained records; completeness is invalidated when a copy is shortened.
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use crate::RetainedContext;
use crate::RetainedContextEntry;
use crate::RetainedUserMessage;
use super::Ordered;
/// The original message, including its role and turn namespace.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RetainedSourceId {
pub message_id: String,
pub turn_id: String,
pub role: RetainedSourceRole,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum RetainedSourceRole {
User,
Assistant,
}
/// A host-observed version, never a fingerprint of rendered prompt text.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct RetainedSource {
pub id: RetainedSourceId,
/// A fresh opaque version prevents reuse across history resets and forks.
pub revision: codex_protocol::ResponseItemId,
pub complete: bool,
}
impl Ordered<RetainedUserMessage> {
pub(super) fn source(&self, role: RetainedSourceRole) -> Option<RetainedSource> {
Some(RetainedSource {
id: RetainedSourceId {
message_id: self.value.message_id.clone()?,
turn_id: self.value.turn_id.clone(),
role,
},
revision: self.revision.clone()?,
complete: self.value.complete,
})
}
}
impl RetainedContext {
/// Restores a host-captured version while replaying its original message.
/// Live recording must mint a new revision for changed evidence instead.
pub fn restore_source_revision(&mut self, source: &RetainedSource) -> bool {
let entries = match source.id.role {
RetainedSourceRole::User => &mut self.user_messages,
RetainedSourceRole::Assistant => &mut self.assistant_messages,
};
let Some(entry) = entries.iter_mut().find(|entry| {
entry.value.message_id.as_deref() == Some(source.id.message_id.as_str())
&& entry.value.turn_id == source.id.turn_id
&& entry.value.complete == source.complete
}) else {
return false;
};
entry.revision = Some(source.revision.clone());
true
}
/// Missing legacy revisions or message IDs cannot establish delivery.
pub fn source(&self, entry: RetainedContextEntry<'_>) -> Option<RetainedSource> {
let (message, role, entries) = match entry {
RetainedContextEntry::UserMessage(message) => {
(message, RetainedSourceRole::User, &self.user_messages)
}
RetainedContextEntry::AssistantMessage(message) => (
message,
RetainedSourceRole::Assistant,
&self.assistant_messages,
),
RetainedContextEntry::VerifiedAnswer(_) => return None,
};
entries
.iter()
.find(|entry| std::ptr::eq(&entry.value, message))?
.source(role)
}
}