Hmbown/CodeWhale · error
Saved session changed after this thread's checkpoint…
Error message
Saved session {session_id} changed after this thread's checkpoint; re-import it into a separate thread to preserve both histories What it means
When resuming a thread that imported a saved session, the code checkpoints a sha256 of the saved session's messages. If the on-disk session file has since changed, resuming here would silently merge two divergent histories, so it refuses and tells you to import the changed session into a separate thread.
Solutions
- Restore the session file to the exact content captured at the checkpoint (undo edits or re-copy the original)
- Import the changed session into a new thread instead of resuming the existing one, preserving both histories
- Delete the checkpoint (start the thread fresh) only if the old history is not needed
Example fix
// before (fails because session file changed)
runtime.resume_thread(&thread, "sess-1").await?;
// after
let fresh = runtime.import_session_thread("sess-1").await?; // separate thread keeps both histories Defensive patterns
Strategy: validation
Validate before calling
fn session_unchanged(checkpoint_sha: &str, session_path: &Path) -> anyhow::Result<bool> {
Ok(checkpoint_sha == sha256_hex(&std::fs::read(session_path)?))
} Try / catch
match runtime.resume_thread(&thread, session_id).await {
Err(e) if e.to_string().contains("changed after this thread's checkpoint") => {
let t = runtime.import_session_thread(session_id).await?;
}
other => other?,
} Prevention
- Never edit a session file while a thread checkpoint references it
- Compute and compare the messages sha256 before resuming
- Import modified sessions into fresh threads
When it happens
Trigger: Calling the resume/import-thread API with a session_id whose saved session file was modified after the thread's saved_session_checkpoint was recorded (messages_sha256 mismatch).
Common situations: Editing or trimming a session JSONL while a thread referencing it is still open; another tool or another machine rewrote the session file; restoring a session from backup with different content.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- Ambiguous prefix ' ' matches sessions
- Cannot open session : its queued input is already open in…
- Cannot open session : its queued input is already open in…
- Checkpoint schema v is newer than supported v
- Checksum manifest is missing
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/566a9200b4e70902.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/runtime_threads.rs:11276
messages.extend(self.reconstruct_messages_from_turns(&turns[covered..])?);
Ok(messages)
}
fn saved_session_prefix(
&self,
thread: &ThreadRecord,
turns: &[TurnRecord],
) -> Result<Option<(Vec<Message>, usize)>> {
let Some(session_id) = thread.session_id.as_deref() else {
return Ok(None);
};
let session = crate::session_manager::default_sessions_dir()
.and_then(crate::session_manager::SessionManager::new)
.and_then(|manager| manager.resume_session(session_id).map(|recovery| recovery.session))
.with_context(|| format!("Cannot read saved session {session_id}; restore that session file before resuming thread {}", thread.id))?;
let covered = if let Some(checkpoint) = &thread.saved_session_checkpoint {
if checkpoint.messages_sha256 != session_messages_sha256(&session.messages)? {
bail!(
"Saved session {session_id} changed after this thread's checkpoint; re-import it into a separate thread to preserve both histories"
);
}
match checkpoint.covered_turn_id.as_deref() {
Some(id) => turns.iter().position(|turn| turn.id == id)
.map(|index| index + 1)
.with_context(|| format!("Saved session checkpoint turn {id} is missing; restore the Runtime turn records before resuming"))?,
None => 0,
}
} else {
// Legacy links have no cursor. Establish it only from an exact
// match to the existing seeder's projection; never use mtime or
// silently let a saved file replace a newer Runtime transcript.
let expected = session_recovery_projection(&session.messages);
let mut prefix = Vec::new();
let mut covered = (expected.is_empty()).then_some(0);
for (index, turn) in turns.iter().enumerate() {
if covered.is_some() {View on GitHub (pinned to 73e0f67d83)