Fix turn context reconstruction after backtracking (#14616)

## Summary
- reuse rollout reconstruction when applying a backtrack rollback so
`reference_context_item` is restored from persisted rollout state
- build rollback replay from the flushed rollout items plus the rollback
marker, avoiding the extra reread/fallback path
- add regression coverage for rollback after compaction so turn-context
diffing stays aligned after backtracking

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charley Cunningham
2026-03-13 19:28:31 -07:00
committed by GitHub
Unverified
parent 69c8a1ef9e
commit bbd329a812
2 changed files with 124 additions and 66 deletions
+93
View File
@@ -1204,6 +1204,99 @@ async fn thread_rollback_recomputes_previous_turn_settings_and_reference_context
);
}
#[tokio::test]
async fn thread_rollback_restores_cleared_reference_context_item_after_compaction() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;
attach_rollout_recorder(&sess).await;
let first_context_item = tc.to_turn_context_item();
let first_turn_id = first_context_item
.turn_id
.clone()
.expect("turn context should have turn_id");
let compact_turn_id = "compact-turn".to_string();
let rolled_back_turn_id = "rolled-back-turn".to_string();
let compacted_history = vec![
user_message("turn 1 user"),
user_message("summary after compaction"),
];
sess.persist_rollout_items(&[
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: first_turn_id.clone(),
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
)),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "turn 1 user".to_string(),
images: None,
local_images: Vec::new(),
text_elements: Vec::new(),
})),
RolloutItem::TurnContext(first_context_item.clone()),
RolloutItem::ResponseItem(user_message("turn 1 user")),
RolloutItem::ResponseItem(assistant_message("turn 1 assistant")),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: first_turn_id,
last_agent_message: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: compact_turn_id.clone(),
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
)),
RolloutItem::Compacted(CompactedItem {
message: "summary after compaction".to_string(),
replacement_history: Some(compacted_history.clone()),
}),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: compact_turn_id,
last_agent_message: None,
})),
RolloutItem::EventMsg(EventMsg::TurnStarted(
codex_protocol::protocol::TurnStartedEvent {
turn_id: rolled_back_turn_id.clone(),
model_context_window: Some(128_000),
collaboration_mode_kind: ModeKind::Default,
},
)),
RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent {
message: "turn 2 user".to_string(),
images: None,
local_images: Vec::new(),
text_elements: Vec::new(),
})),
RolloutItem::TurnContext(TurnContextItem {
turn_id: Some(rolled_back_turn_id.clone()),
model: "rolled-back-model".to_string(),
..first_context_item.clone()
}),
RolloutItem::ResponseItem(user_message("turn 2 user")),
RolloutItem::ResponseItem(assistant_message("turn 2 assistant")),
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
turn_id: rolled_back_turn_id,
last_agent_message: None,
})),
])
.await;
sess.replace_history(
vec![assistant_message("stale history")],
Some(first_context_item),
)
.await;
handlers::thread_rollback(&sess, "sub-1".to_string(), 1).await;
let rollback_event = wait_for_thread_rolled_back(&rx).await;
assert_eq!(rollback_event.num_turns, 1);
assert_eq!(sess.clone_history().await.raw_items(), compacted_history);
assert!(sess.reference_context_item().await.is_none());
}
#[tokio::test]
async fn thread_rollback_persists_marker_and_replays_cumulatively() {
let (sess, tc, rx) = make_session_and_context_with_rx().await;