fix(tui): refuse a backtrack boundary until every kept prompt is matched

saved_history_boundary accepted the undone turn's prompt while a kept
prompt was still unmatched, so a transcript that drifted from the records
(a kept turn missing) was cut at the wrong place and lost a kept turn.
Now any mismatch before the kept prefix is complete refuses, and the
caller preserves the source thread.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-23 22:34:22 -07:00
1 parent 47aa0fdb9f
commit e941d370af
2 files changed
+34 -3

No files matched your search

+7 -3
View File
@@ -832,9 +832,13 @@ fn saved_history_boundary(
};
match expected {
Some(next) if next == &text => expected = kept.next(),
// The first user text that is not part of the kept prefix is where
// the undone turn begins — and it has to be that turn's own prompt.
_ => return (text == target_prompt).then_some(index),
// A kept prompt is still unaccounted for, so this transcript has
// drifted from the records; even the undone turn's prompt here
// would cut away a turn the backtrack must keep.
Some(_) => return None,
// The first user text after the whole kept prefix is where the
// undone turn begins — and it has to be that turn's own prompt.
None => return (text == target_prompt).then_some(index),
}
}
None
+27
View File
@@ -17483,3 +17483,30 @@ async fn flush_recovery_receipts_drains_every_listed_thread() -> Result<()> {
Ok(())
}
#[test]
fn saved_history_boundary_refuses_until_every_kept_prompt_is_seen() {
let user = |text: &str| Message {
role: Role::User,
content: vec![ContentBlock::Text {
text: text.to_string(),
cache_control: None,
}],
};
let kept = vec!["first".to_string(), "second".to_string()];
// The kept prompts in order, then the undone turn's prompt: cut there.
let messages = vec![user("first"), user("second"), user("undo me")];
assert_eq!(saved_history_boundary(&messages, &kept, "undo me"), Some(2));
// "second" never appears: matching "undo me" early would drop a kept turn.
let drifted = vec![user("first"), user("undo me"), user("second")];
assert_eq!(saved_history_boundary(&drifted, &kept, "undo me"), None);
// A repeated prompt stays aligned with the records.
let repeated = vec![user("same"), user("same")];
assert_eq!(
saved_history_boundary(&repeated, &["same".to_string()], "same"),
Some(1)
);
}