Preserve verified answers across history compaction (#42293)

## Why

Compaction replaces the model history window, but host-verified user answers may
still be needed for the rest of the owning thread.

## What changed

- Add bounded, model-invisible retained context for verified
  `request_user_input` answers.
- Persist retained context in rollout events and compaction checkpoints, and
  restore it when a thread resumes.
- Remove answers when their source turns are rolled back, and prevent
  parent-local answers from being inherited by child forks.

## Testing

Add lifecycle coverage for deduplication and size limits, compaction and resume,
rollback, rollout migration, and child forks.

GitOrigin-RevId: c32900d3e8120a9899b71f0576683acd67a4b898
This commit is contained in:
felixxia-oai authored and copyberry committed 2026-09-02 15:07:11 +00:00
1 parent 389dd56459
commit 5971d42847
45 files changed
+805 -9

No files matched your search

+1
View File
@@ -29,6 +29,7 @@ pub use codex_history::CompactedItem;
pub use codex_history::InitialHistory;
pub use codex_history::ResponseItemEnvelope;
pub use codex_history::ResumedHistory;
pub use codex_history::RetainedContextEvent;
pub use codex_history::RolloutItem;
pub use codex_history::RolloutLine;
pub(crate) use codex_protocol::protocol;
+5 -1
View File
@@ -1184,7 +1184,9 @@ async fn read_head_summary(path: &Path, head_limit: usize) -> io::Result<HeadTai
RolloutItem::TokenUsageRecord(_) => {
// Not included in `head`; skip.
}
RolloutItem::WorldState(_) | RolloutItem::SecurityRiskScore(_) => {
RolloutItem::RetainedContext(_)
| RolloutItem::WorldState(_)
| RolloutItem::SecurityRiskScore(_) => {
// Not included in `head`; skip.
}
RolloutItem::RealtimeItem(_) => {
@@ -1262,6 +1264,7 @@ pub async fn read_head_for_summary(path: &Path) -> io::Result<Vec<serde_json::Va
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::WorldState(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::EventMsg(_) => {}
}
@@ -1317,6 +1320,7 @@ pub async fn read_session_meta_line(path: &Path) -> io::Result<SessionMetaLine>
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::WorldState(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::EventMsg(_) => {}
}
+2
View File
@@ -78,6 +78,7 @@ pub fn builder_from_items(
| RolloutItem::WorldState(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::EventMsg(_) => None,
}) && let Some(builder) = builder_from_session_meta(session_meta, rollout_path)
@@ -169,6 +170,7 @@ pub async fn extract_metadata_from_rollout(
| RolloutItem::WorldState(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::EventMsg(_) => None,
}),
+1
View File
@@ -291,6 +291,7 @@ fn builder_from_items_falls_back_to_filename() {
let items = vec![RolloutItem::Compacted(CompactedItem {
message: "noop".to_string(),
replacement_history: None,
retained_context: None,
guardian_history: None,
mcp_resource_origins: None,
window_number: None,
+1
View File
@@ -160,6 +160,7 @@ impl ModelContextScan {
| RolloutItem::SessionMeta(_)
| RolloutItem::InterAgentCommunicationMetadata { .. }
| RolloutItem::RealtimeItem(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::TokenUsageRecord(_) => {}
}
@@ -237,6 +237,7 @@ fn rollout_item_type(item: &RolloutItem) -> String {
RolloutItem::TurnContext(_) => "turn_context".to_string(),
RolloutItem::TokenUsageRecord(_) => "token_usage_record".to_string(),
RolloutItem::WorldState(_) => "world_state".to_string(),
RolloutItem::RetainedContext(_) => "retained_context".to_string(),
RolloutItem::SecurityRiskScore(_) => "security_risk_score".to_string(),
RolloutItem::RealtimeItem(item) => match &item.content {
codex_protocol::realtime::RealtimeItemContent::RealtimeSessionStarted => {
+1
View File
@@ -19,6 +19,7 @@ pub fn is_persisted_rollout_item(item: &RolloutItem, history_mode: ThreadHistory
| RolloutItem::TurnContext(_)
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::WorldState(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::SessionMeta(_) => true,
}
+1
View File
@@ -2099,6 +2099,7 @@ async fn resume_candidate_matches_cwd(
| RolloutItem::WorldState(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::EventMsg(_) => None,
})
+1
View File
@@ -293,6 +293,7 @@ fn conversation_text_from_item(item: &RolloutItem) -> Option<String> {
| RolloutItem::InterAgentCommunicationMetadata { .. }
| RolloutItem::Compacted(_)
| RolloutItem::RealtimeItem(_)
| RolloutItem::RetainedContext(_)
| RolloutItem::SecurityRiskScore(_)
| RolloutItem::TokenUsageRecord(_)
| RolloutItem::WorldState(_) => None,