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
- Open the ledger file shown in the path and validate it (jq . <ledger.json>) to find the malformed section; repair or regenerate it.
- Delete or archive the corrupt ledger file so the manager recreates an empty one for that session.
- 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.
- 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
- Never hand-edit ledger JSON; use the library's write path (atomic writes) only.
- Validate JSON with jq or a serde pre-parse after any external tool touches the file.
- Monitor for truncated files after crashes; write ledgers atomically (write_atomic).
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
- (serde_json deserialization error wrapped as…
- (serde_json deserialization error wrapped as…
- deserialize
- Fleet task has an invalid durable member snapshot
- roundtrip
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)