Hmbown/CodeWhale · error

{err}

Error message

{err}

What it means

hydrate_approval_receipts loads durable approval receipts for a session and then replays them through ApprovalReplay::from_receipts; a replay failure is converted into io::ErrorKind::InvalidData. It means the stored receipt set is internally inconsistent (e.g. unmatched or malformed approval records) and cannot be reconstructed into a valid approval state. This fires during hydration for resume paths.

Solutions

  1. Read the chained source() error from ApprovalReplay to identify the malformed receipt
  2. Remove or repair the offending approval receipt entries in the session's receipt store
  3. Re-save the session so receipts are regenerated; or restore the receipt store from backup
  4. Check for version drift: receipts written by an older release may need migration
Defensive patterns

Strategy: try-catch

Validate before calling

ApprovalReplay::from_receipts(&session.approval_receipts).map_err(|e| format!("bad receipts: {e}"))?;

Try / catch

match manager.hydrate_approval_receipts(&mut session) {
    Ok(()) => resume(session),
    Err(e) => {
        log::warn!("receipt replay failed: {e}; starting without durable approvals");
        session.approval_receipts.clear(); // fallback: drop corrupt receipts
    }
}

Prevention

When it happens

Trigger: Calling hydrate_approval_receipts when either approval_receipt_store().load succeeded but the receipts fail ApprovalReplay::from_receipts — corrupted, partial, or schema-drifted receipt entries in the store.

Common situations: Receipt files edited or truncated by a crash mid-write; receipts written by an older codewhale version whose format changed; manually copying approval receipt data between sessions.

Understand the failure class

Background: "failed to unmarshal" / json.Unmarshal errors: why parsing a response into a Go struct fails and how to fix it — this error's family across 23 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/session_manager.rs:1254

/// Per-session offline queue file: `checkpoints/<session_id>.offline_queue.json`.
const OFFLINE_QUEUE_SUFFIX: &str = ".offline_queue.json";

pub(crate) fn is_offline_queue_file(name: &str) -> bool {
    name == OFFLINE_QUEUE_FILE || name.ends_with(OFFLINE_QUEUE_SUFFIX)
}

impl SessionManager {
    fn approval_receipt_store(&self) -> ApprovalReceiptStore {
        ApprovalReceiptStore::new(self.sessions_dir.clone())
    }

    fn hydrate_approval_receipts(&self, session: &mut SavedSession) -> io::Result<()> {
        let durable = self.approval_receipt_store().load(&session.metadata.id)?;
        if !durable.is_empty() {
            session.approval_receipts = durable;
        }
        ApprovalReplay::from_receipts(&session.approval_receipts)
            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
        Ok(())
    }

    /// Reconstruct completed approvals and interrupted unmatched asks for one
    /// session without consulting the model transcript.
    #[cfg_attr(not(test), expect(dead_code))]
    pub(crate) fn replay_approvals(&self, session_id: &str) -> io::Result<ApprovalReplay> {
        self.approval_receipt_store().replay(session_id)
    }

    fn validated_session_id<'a>(&self, id: &'a str) -> std::io::Result<&'a str> {
        let trimmed = id.trim();
        if trimmed.is_empty() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "Session id cannot be empty",
            ));
        }

View on GitHub (pinned to 73e0f67d83)