Hmbown/CodeWhale · error · io::Error

<serde_json deserialization error>

Error message

<serde_json deserialization error>

What it means

Loading a session's late-usage ledger from disk failed because the file's bytes are not valid JSON matching the LateUsageLedger schema; serde_json::from_slice returned a parse/deser error which is wrapped as an io::Error of kind InvalidData. The library refuses to tolerate a corrupt or hand-edited ledger file rather than silently starting an empty ledger. The wrapped serde message names the exact offending JSON path or type mismatch.

Solutions

  1. Open the ledger file shown in the path and validate it (jq . <ledger.json>) to find the malformed section; repair or regenerate it.
  2. Delete or archive the corrupt ledger file so the manager recreates an empty one for that session.
  3. If you produced the file externally, write it with serde_json::to_string for the same LateUsageLedger struct and the current CURRENT_LATE_USAGE_SCHEMA_VERSION.
  4. Check the file is not truncated at the byte-size bound (MAX_LATE_USAGE_LEDGER_BYTES) — a partially written file fails JSON parsing.

Example fix

// before (corrupt hand-edited ledger)
{ "schema_version": "1", "records": [] }
// after (matches LateUsageLedger)
{ "schema_version": 1, "records": [] }
Defensive patterns

Strategy: validation

Validate before calling

let raw = std::fs::read(&ledger_path)?;
if raw.is_empty() || std::str::from_utf8(&raw).map(|s| serde_json::from_str::<serde_json::Value>(s)).map_err(|_| ()).is_err() {
    // corrupt or empty: repair, regenerate, or delete before loading
}
let _ok: LateUsageLedger = serde_json::from_slice(&raw)?; // pre-flight parse

Type guard

fn is_valid_ledger(bytes: &[u8]) -> bool {
    serde_json::from_slice::<LateUsageLedger>(bytes).is_ok()
}

Try / catch

match manager.load_late_usage(&session_id) {
    Ok(l) => l,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        // quarantine corrupt ledger, fall back to a fresh ledger
        LateUsageLedger::default()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling load_late_usage / load_late_usage_unlocked when the on-disk ledger file contains malformed JSON, a wrong schema (missing schema_version or records fields), or fields whose types do not match the struct (e.g. records as an object, schema_version as a string).

Common situations: A crash or full disk truncated the ledger mid-write; a user or script hand-edited or deleted part of the JSON; a ledger written by a different (older or newer) version of the schema whose field shapes differ.

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/ac90588a60c8a275. Report an issue: GitHub.

Appendix: source

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

            ));
        }
        use std::io::Read as _;
        let mut raw = Vec::with_capacity(
            usize::try_from(metadata.len().min(MAX_LATE_USAGE_LEDGER_BYTES)).unwrap_or(0),
        );
        file.take(MAX_LATE_USAGE_LEDGER_BYTES.saturating_add(1))
            .read_to_end(&mut raw)?;
        if u64::try_from(raw.len()).unwrap_or(u64::MAX) > MAX_LATE_USAGE_LEDGER_BYTES {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "late usage ledger {} exceeds its size bound",
                    path.display()
                ),
            ));
        }
        let ledger: LateUsageLedger = serde_json::from_slice(&raw)
            .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
        if ledger.schema_version != CURRENT_LATE_USAGE_SCHEMA_VERSION
            || ledger.records.len() > MAX_LATE_USAGE_RECORDS_PER_SESSION
            || ledger.records.iter().any(|record| {
                !is_sha256_fingerprint(&record.source_fingerprint)
                    || !is_sha256_fingerprint(&record.turn_fingerprint)
            })
        {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                "late usage ledger has an unsupported or unbounded shape",
            ));
        }
        Ok(ledger)
    }

    fn with_session_write_admission<T>(
        &self,
        session_id: &str,

View on GitHub (pinned to 73e0f67d83)