Preserve contextual session prefix on rollback

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-25 11:40:08 -07:00
parent 287f030978
commit 5e70ae9de0

View File

@@ -244,7 +244,8 @@ impl ContextManager {
user_positions[user_positions.len() - n_from_end]
};
cut_idx = self.trim_pre_turn_context_updates(&snapshot, cut_idx);
cut_idx =
self.trim_pre_turn_context_updates(&snapshot, first_instruction_turn_idx, cut_idx);
self.replace(snapshot[..cut_idx].to_vec());
}
@@ -400,10 +401,14 @@ impl ContextManager {
/// Returns the adjusted cut index after removing contextual developer/user items immediately
/// above the rolled-back turn boundary.
///
/// `first_instruction_turn_idx` is the earliest rollback-eligible instruction-turn boundary
/// in `snapshot`. When rolling back the first real turn, a fully contextual prefix is treated
/// as session bootstrap and is therefore preserved.
///
/// `cut_idx` is the tentative slice boundary after dropping the requested number of
/// instruction turns, before stripping contextual pre-turn items that sit immediately above
/// that boundary. The trim walk may continue all the way to index `0`, but it stops as soon
/// as it encounters a non-contextual item, so only actual per-turn scaffolding is removed.
/// that boundary. The trim walk stops as soon as it encounters a non-contextual item, so only
/// actual per-turn scaffolding is removed.
///
/// If any trimmed developer message was a mixed `build_initial_context` bundle containing both
/// rollback-trimmable contextual fragments and persistent developer text, this also clears the
@@ -412,18 +417,18 @@ impl ContextManager {
fn trim_pre_turn_context_updates(
&mut self,
snapshot: &[ResponseItem],
first_instruction_turn_idx: usize,
mut cut_idx: usize,
) -> usize {
let original_cut_idx = cut_idx;
let mut trimmed_mixed_dev_bundle = false;
while cut_idx > 0 {
match &snapshot[cut_idx - 1] {
ResponseItem::Message { role, content, .. }
if role == "developer" && is_contextual_dev_message_content(content) =>
{
if has_non_contextual_dev_message_content(content) {
// Mixed `build_initial_context` bundles are not reconstructible from
// steady-state diffs once trimmed, so the next real turn must fully
// reinject context instead of diffing against a stale baseline.
self.reference_context_item = None;
trimmed_mixed_dev_bundle = true;
}
cut_idx -= 1;
}
@@ -435,6 +440,18 @@ impl ContextManager {
_ => break,
}
}
if original_cut_idx == first_instruction_turn_idx && cut_idx == 0 {
return original_cut_idx;
}
if trimmed_mixed_dev_bundle {
// Mixed `build_initial_context` bundles are not reconstructible from steady-state
// diffs once trimmed, so the next real turn must fully reinject context instead of
// diffing against a stale baseline.
self.reference_context_item = None;
}
cut_idx
}
}