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

  1. Restore the session file to the exact content captured at the checkpoint (undo edits or re-copy the original)
  2. Import the changed session into a new thread instead of resuming the existing one, preserving both histories
  3. 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

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


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)