Hmbown/CodeWhale · error · io::Error

err

Error message

err

What it means

load_unlocked parses all receipt lines and then rebuilds session state via ApprovalReplay::from_receipts. If the receipts are individually valid JSON but their sequence is semantically inconsistent for replay (e.g. a state machine transition that cannot happen), the replay error is wrapped in an io::Error with InvalidData. This is a semantic-validation failure of the approval history, not a parse failure.

Solutions

  1. Inspect the log's receipt order and remove/reorder the offending line identified by replay's error text
  2. Delete the log to reset replay state if the history is not needed
  3. Verify all writers go through ApprovalLog::append (which takes the fd_lock write lock) rather than appending directly
  4. Check for version skew: replay rules may have changed between crate versions
Defensive patterns

Strategy: validation

Validate before calling

let receipts: Vec<ApprovalReceipt> = log.load(session_id)?;
ApprovalReplay::from_receipts(&receipts)?; // surface semantic issues before use

Try / catch

let replay = log.replay(session_id)
    .map_err(|e| e.context("approval history failed replay validation"))?;

Prevention

When it happens

Trigger: load/append call load_unlocked and the parsed receipt sequence violates ApprovalReplay's invariants — out-of-order receipts, a decision referencing an unknown request, or duplicate/conflicting receipts.

Common situations: Log assembled from merged sessions or copied between machines; concurrent writers bypassing the lock file; receipts from a version with different replay rules.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/75c9fc12db205ae5. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/approval_log.rs:239

        let path = self.log_path(session_id)?;
        let file = match File::open(path) {
            Ok(file) => file,
            Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
            Err(err) => return Err(err),
        };
        let mut receipts = Vec::new();
        for (index, line) in BufReader::new(file).lines().enumerate() {
            let line = line?;
            let receipt = serde_json::from_str(&line).map_err(|err| {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!("invalid approval receipt at line {}: {err}", index + 1),
                )
            })?;
            receipts.push(receipt);
        }
        ApprovalReplay::from_receipts(&receipts)
            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
        Ok(receipts)
    }

    pub(crate) fn load(&self, session_id: &str) -> io::Result<Vec<ApprovalReceipt>> {
        if !self.log_path(session_id)?.exists() {
            return Ok(Vec::new());
        }
        let Some(lock_file) = self.open_existing_lock_file(session_id)? else {
            // Imported or legacy snapshots can contain a receipt log without
            // its ephemeral lock file. Preserve read-only session loading;
            // live writers always publish the lock before creating the log.
            return self.load_unlocked(session_id);
        };
        let lock = fd_lock::RwLock::new(lock_file);
        let _guard = lock.read()?;
        self.load_unlocked(session_id)
    }

View on GitHub (pinned to 433685b202)