Resume after rollback of compacted turn replays discarded assistant output

Open 💬 0 comments Opened Jul 13, 2026 by ShiroKSH

Summary

Resuming a thread after rolling back a user turn that compacted mid-turn can replay assistant or tool output from the discarded turn into the model history.

Affected path

On current main (84696e3d57), codex-rs/core/src/session/rollout_reconstruction.rs selects the suffix after a Compacted { replacement_history: Some(_) } marker during the reverse scan. That selection happens before the active segment is finalized. The later ThreadRolledBack handling rejects that segment, but it does not clear the already-selected suffix.

Forward replay therefore starts after the rejected compaction marker. The subsequent rollback has no retained user-message boundary to remove, so post-compaction assistant or tool response items from the rolled-back turn remain in the reconstructed history.

Reproduction shape

Persist rollout items in this order:

  1. A completed surviving user turn.
  2. A second user turn with a user response item.
  3. Compacted with replacement_history: Some(...) within that second turn.
  4. A post-compaction assistant response item.
  5. Completion of the second turn.
  6. ThreadRolledBack { num_turns: 1 }.

Resuming should reconstruct only the first turn. Instead, the post-compaction assistant response item from the rolled-back second turn is retained.

Expected behavior

All model-visible items belonging to a rolled-back user turn should be absent after resume, including items written after an in-turn compaction checkpoint.

Suggested regression and fix

Extend record_initial_history_resumed_rollback_drops_incomplete_user_turn_compaction_metadata in codex-rs/core/src/session/rollout_reconstruction_tests.rs with a post-compaction assistant response item and assert that the reconstructed raw history does not contain it.

Keep a checkpoint's replacement history and suffix on the active replay segment, then publish both only after finalize_active_segment has accepted the segment. The rollback-skip path should discard both together.

View original on GitHub ↗