mirror of
https://github.com/openai/codex.git
synced 2026-11-15 14:52:36 +00:00
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:
1 parent
389dd56459
commit
5971d42847
45 files changed
+805
-9
No files matched your search
@@ -1540,6 +1540,7 @@ impl Session {
|
||||
) -> Option<PreviousTurnSettings> {
|
||||
let rollout_reconstruction::RolloutReconstruction {
|
||||
mut history,
|
||||
retained_context,
|
||||
guardian_history,
|
||||
previous_turn_settings,
|
||||
reference_context_item,
|
||||
@@ -1586,6 +1587,7 @@ impl Session {
|
||||
state
|
||||
.history
|
||||
.restore_guardian_history(guardian_history.as_ref());
|
||||
state.history.restore_retained_context(&retained_context);
|
||||
if let Some(world_state) = world_state_baseline {
|
||||
state.history.set_world_state_baseline(world_state);
|
||||
}
|
||||
@@ -3760,6 +3762,7 @@ impl Session {
|
||||
let mut compacted_item = CompactedItem {
|
||||
message: metadata.message,
|
||||
replacement_history: Some(items.clone()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: self.services.mcp_runtime.resource_origin_checkpoint(),
|
||||
window_number: Some(metadata.window_number),
|
||||
@@ -3785,6 +3788,7 @@ impl Session {
|
||||
HistoryReplacement::Compaction,
|
||||
);
|
||||
compacted_item.guardian_history = state.history.guardian_history_checkpoint();
|
||||
compacted_item.retained_context = Some(state.history.retained_context().clone());
|
||||
if let Some(world_state) = world_state_baseline {
|
||||
let snapshot = world_state.snapshot();
|
||||
world_state_item = Some(WorldStateItem::full(snapshot.clone().into_object()));
|
||||
|
||||
@@ -10,6 +10,7 @@ use uuid::Uuid;
|
||||
#[derive(Debug)]
|
||||
pub(super) struct RolloutReconstruction {
|
||||
pub(super) history: Vec<ResponseItemEnvelope>,
|
||||
pub(super) retained_context: codex_history::RetainedContext,
|
||||
pub(super) guardian_history: Option<codex_history::GuardianHistoryCheckpoint>,
|
||||
pub(super) previous_turn_settings: Option<PreviousTurnSettings>,
|
||||
pub(super) reference_context_item: Option<TurnContextItem>,
|
||||
@@ -300,6 +301,7 @@ impl Session {
|
||||
RolloutItem::EventMsg(_)
|
||||
| RolloutItem::SessionMeta(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::RetainedContext(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::InterAgentCommunicationMetadata { .. } => {}
|
||||
@@ -343,6 +345,9 @@ impl Session {
|
||||
{
|
||||
history.replace_annotated(items.clone());
|
||||
history.restore_guardian_history(checkpoint.compacted.guardian_history.as_ref());
|
||||
if let Some(retained) = &checkpoint.compacted.retained_context {
|
||||
history.restore_retained_context(retained);
|
||||
}
|
||||
}
|
||||
// Materialize exact history semantics from the replay-derived suffix. The eventual lazy
|
||||
// design should keep this same replay shape, but drive it from a resumable reverse source
|
||||
@@ -350,6 +355,9 @@ impl Session {
|
||||
let rollout_suffix = base_compaction.map_or(rollout_items, |checkpoint| checkpoint.suffix);
|
||||
for item in rollout_suffix {
|
||||
match item {
|
||||
RolloutItem::RetainedContext(event) => {
|
||||
history.record_retained_context(event);
|
||||
}
|
||||
RolloutItem::ResponseItem(response_item) => {
|
||||
history.record_annotated_items(
|
||||
std::slice::from_ref(response_item),
|
||||
@@ -385,7 +393,9 @@ impl Session {
|
||||
&user_messages,
|
||||
&compacted.message,
|
||||
);
|
||||
let retained_context = history.retained_context().clone();
|
||||
history.replace_annotated(rebuilt);
|
||||
history.restore_retained_context(&retained_context);
|
||||
}
|
||||
}
|
||||
RolloutItem::EventMsg(EventMsg::ThreadRolledBack(rollback)) => {
|
||||
@@ -437,6 +447,7 @@ impl Session {
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::RetainedContext(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::EventMsg(_) => {
|
||||
unreachable!("only world-state replay items are collected")
|
||||
@@ -451,6 +462,7 @@ impl Session {
|
||||
id: None,
|
||||
});
|
||||
RolloutReconstruction {
|
||||
retained_context: history.retained_context().clone(),
|
||||
guardian_history: history.guardian_history_checkpoint(),
|
||||
history: history.into_annotated_items(),
|
||||
previous_turn_settings,
|
||||
|
||||
@@ -1061,6 +1061,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1124,6 +1125,7 @@ async fn record_initial_history_requires_surviving_full_snapshot_without_user_tu
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1159,6 +1161,7 @@ async fn record_initial_history_resumed_does_not_seed_reference_context_item_aft
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1232,6 +1235,7 @@ async fn reconstruct_history_prefers_compacted_window_over_session_meta() {
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(2),
|
||||
@@ -1271,6 +1275,7 @@ async fn reconstruct_history_replays_world_state_from_latest_compaction_window()
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(1),
|
||||
@@ -1336,6 +1341,7 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions() {
|
||||
replacement_history: Some(annotated(vec![assistant_message(&format!(
|
||||
"summary-{window_number}"
|
||||
))])),
|
||||
retained_context: None,
|
||||
guardian_history: Some(codex_history::GuardianHistoryCheckpoint(vec![
|
||||
user_message("original task"),
|
||||
])),
|
||||
@@ -1446,6 +1452,7 @@ async fn reconstruct_history_preserves_legacy_compaction_count_with_session_meta
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: "legacy summary".to_string(),
|
||||
replacement_history: None,
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1471,12 +1478,25 @@ async fn reconstruct_history_preserves_legacy_compaction_count_with_session_meta
|
||||
async fn reconstruct_history_legacy_compaction_without_replacement_history_does_not_inject_current_initial_context()
|
||||
{
|
||||
let (session, turn_context) = make_session_and_context().await;
|
||||
let answer =
|
||||
codex_history::RetainedContextEvent::VerifiedAnswer(codex_history::VerifiedAnswer {
|
||||
turn_id: "legacy-turn".to_owned(),
|
||||
call_id: "ask-1".to_owned(),
|
||||
questions: vec![codex_history::VerifiedQuestionAnswer {
|
||||
question: "Upload?".to_owned(),
|
||||
answer: "Only privately.".to_owned(),
|
||||
}],
|
||||
});
|
||||
let mut retained = codex_history::RetainedContext::default();
|
||||
retained.record(&answer);
|
||||
let rollout_items = vec![
|
||||
RolloutItem::ResponseItem(user_message("before compact").into()),
|
||||
RolloutItem::ResponseItem(assistant_message("assistant reply").into()),
|
||||
RolloutItem::RetainedContext(answer),
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: "legacy summary".to_string(),
|
||||
replacement_history: None,
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1500,6 +1520,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_does_
|
||||
])
|
||||
);
|
||||
assert!(reconstructed.reference_context_item.is_none());
|
||||
assert_eq!(reconstructed.retained_context, retained);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -1516,6 +1537,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: "legacy summary".to_string(),
|
||||
replacement_history: None,
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1624,6 +1646,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -1796,6 +1819,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -2054,6 +2078,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -2232,6 +2257,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(Vec::new()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
|
||||
@@ -1369,6 +1369,7 @@ impl Session {
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::RetainedContext(_)
|
||||
| RolloutItem::SecurityRiskScore(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2224,6 +2224,7 @@ async fn reconstruct_history_uses_replacement_history_verbatim() {
|
||||
let rollout_items = vec![RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: Some(replacement_history.clone()),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(42),
|
||||
@@ -2905,6 +2906,7 @@ fn latest_token_usage_record_stops_at_compaction_checkpoint() {
|
||||
RolloutItem::Compacted(CompactedItem {
|
||||
message: String::new(),
|
||||
replacement_history: None,
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: None,
|
||||
@@ -3516,6 +3518,7 @@ async fn start_new_context_window_persists_checkpoint_state() {
|
||||
| RolloutItem::InterAgentCommunicationMetadata { .. }
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::RetainedContext(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
@@ -3605,6 +3608,7 @@ async fn record_initial_history_assigns_and_persists_id_for_forked_response_item
|
||||
| RolloutItem::Compacted(_)
|
||||
| RolloutItem::TurnContext(_)
|
||||
| RolloutItem::WorldState(_)
|
||||
| RolloutItem::RetainedContext(_)
|
||||
| RolloutItem::SecurityRiskScore(_)
|
||||
| RolloutItem::TokenUsageRecord(_)
|
||||
| RolloutItem::RealtimeItem(_)
|
||||
@@ -4170,6 +4174,7 @@ async fn thread_rollback_restores_cleared_reference_context_item_after_compactio
|
||||
.map(ResponseItemEnvelope::new)
|
||||
.collect(),
|
||||
),
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(7),
|
||||
@@ -12447,6 +12452,7 @@ async fn sample_rollout(
|
||||
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
||||
message: summary1.to_string(),
|
||||
replacement_history: None,
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(window_number),
|
||||
@@ -12480,6 +12486,7 @@ async fn sample_rollout(
|
||||
rollout_items.push(RolloutItem::Compacted(CompactedItem {
|
||||
message: summary2.to_string(),
|
||||
replacement_history: None,
|
||||
retained_context: None,
|
||||
guardian_history: None,
|
||||
mcp_resource_origins: None,
|
||||
window_number: Some(window_number),
|
||||
|
||||
Reference in new issue
Block a user