Hmbown/CodeWhale · error

Saved session checkpoint exceeds its transcript; restore…

Error message

Saved session checkpoint exceeds its transcript; restore the saved session before resuming

What it means

A thread's saved_session_checkpoint can pin how many transcript messages were retained at checkpoint time. If that retained count exceeds the number of messages actually present in the in-memory transcript, the checkpoint is inconsistent with the transcript and resuming would fabricate history, so the operation bails and asks you to restore the saved session first.

Solutions

  1. Restore the saved session (full transcript) and resume from that restored copy
  2. Clear or recompute the thread's saved_session_checkpoint so it matches the current transcript
  3. Re-import the session into a new thread instead of resuming the truncated one

Example fix

// before (checkpoint expects 50 messages, transcript has 30)
runtime.resume_thread(&thread, "sess-1").await?;
// after
let restored = runtime.restore_session("sess-1").await?;
runtime.resume_thread(&restored.into_thread(), "sess-1").await?;
Defensive patterns

Strategy: validation

Validate before calling

if let Some(cp) = &thread.saved_session_checkpoint {
    if cp.retained_messages.map_or(false, |r| r > transcript_len) {
        return Err(anyhow!("checkpoint exceeds transcript; restore session first"));
    }
}

Try / catch

match runtime.resume_thread(&thread, id).await {
    Err(e) if e.to_string().contains("checkpoint exceeds its transcript") => {
        let restored = runtime.restore_session(id).await?;
        runtime.resume_thread(&restored.into_thread(), id).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Resuming a thread whose saved_session_checkpoint.retained_messages is greater than messages.len() — e.g. the transcript was truncated, cleared, or a different/older transcript was loaded before resume.

Common situations: Manually editing or pruning a session transcript; loading a partial session file; a crash or migration that dropped turns from the in-memory thread while the checkpoint still references the full count.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/e53d0bc85912ad78. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/runtime_threads.rs:11315

                prefix.extend(session_recovery_projection(
                    &self.reconstruct_messages_from_turns(std::slice::from_ref(turn))?,
                ));
                if prefix == expected {
                    covered = Some(index + 1);
                } else if !expected.starts_with(&prefix) {
                    break;
                }
            }
            covered.with_context(|| format!("Saved session {session_id} has no verifiable Runtime checkpoint; keep both histories and re-import the saved session into a separate thread"))?
        };
        let mut messages = session.messages;
        if let Some(retained) = thread
            .saved_session_checkpoint
            .as_ref()
            .and_then(|checkpoint| checkpoint.retained_messages)
        {
            if retained > messages.len() {
                bail!(
                    "Saved session checkpoint exceeds its transcript; restore the saved session before resuming"
                );
            }
            messages.truncate(retained);
        }
        Ok(Some((messages, covered)))
    }

    fn reconstruct_messages_from_turns(&self, turns: &[TurnRecord]) -> Result<Vec<Message>> {
        let mut messages = Vec::new();
        for turn in turns {
            let stored_items = self.store.list_items_for_turn(&turn.id)?;
            let items = if turn.item_ids.is_empty() {
                stored_items
            } else {
                let mut by_id: HashMap<String, TurnItemRecord> = stored_items
                    .iter()
                    .cloned()

View on GitHub (pinned to 73e0f67d83)