Hmbown/CodeWhale · error · io::Error

late usage ledger has an unsupported or unbounded shape

Error message

late usage ledger has an unsupported or unbounded shape

What it means

The ledger parsed as JSON but its shape is rejected: schema_version does not equal CURRENT_LATE_USAGE_SCHEMA_VERSION, records exceed MAX_LATE_USAGE_RECORDS_PER_SESSION, or any record's source_fingerprint/turn_fingerprint is not a valid SHA-256 fingerprint. This is a semantic validation error (InvalidData) guarding against unbounded or future-format ledgers.

Solutions

  1. Check the ledger's schema_version against CURRENT_LATE_USAGE_SCHEMA_VERSION in session_manager.rs and migrate or regenerate the file to the current version.
  2. Trim records to the per-session cap; keep the most recent MAX_LATE_USAGE_RECORDS_PER_SESSION entries.
  3. Fix fingerprint fields to be valid SHA-256 hex (64 lowercase hex chars) — recompute them from the source/turn content if they were altered.
  4. Delete the out-of-shape ledger so the session starts with a fresh, empty ledger.

Example fix

// before (rejected record)
{ "source_fingerprint": "abc123", "turn_fingerprint": "def456" }
// after (full SHA-256 hex)
{ "source_fingerprint": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08", "turn_fingerprint": "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752" }
Defensive patterns

Strategy: validation

Validate before calling

let v: serde_json::Value = serde_json::from_slice(&raw)?;
let schema_ok = v["schema_version"].as_u64() == Some(CURRENT_LATE_USAGE_SCHEMA_VERSION as u64);
let records_ok = v["records"].as_array().map_or(false, |r| r.len() <= MAX_LATE_USAGE_RECORDS_PER_SESSION);
let fp_ok = |s: &str| s.len() == 64 && s.bytes().all(|b| b.is_ascii_hexdigit());
if !(schema_ok && records_ok) { /* migrate, trim, or reject before loading */ }

Type guard

fn is_sha256_fingerprint(s: &str) -> bool {
    s.len() == 64 && s.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F'))
}

Try / catch

match manager.load_late_usage(&session_id) {
    Ok(l) => l,
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => {
        // shape mismatch: archive the file, start a fresh ledger or run a schema migration
        archive_and_reset(&ledger_path);
        LateUsageLedger::default()
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Loading a ledger whose schema_version differs from the current constant, a ledger with more records than the per-session cap, or records whose fingerprint strings are not 64-char SHA-256 hex values.

Common situations: App upgraded/downgraded so schema_version changed; a runaway loop appended thousands of records before the cap existed; fingerprints were written as empty strings, prefixed hashes, or different hash encodings by external tooling.

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@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/5733c565598bea4b. Report an issue: GitHub.

Appendix: source

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

        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,
        write: impl FnOnce() -> io::Result<T>,
    ) -> io::Result<Option<T>> {
        let (path, lock_path) = self.ensure_late_usage_paths(session_id)?;
        let lock_file = open_private_lock_file(&lock_path)?;
        let mut lock = fd_lock::RwLock::new(lock_file);
        let _guard = lock.write()?;
        if Self::late_usage_is_deleted(&path)? {
            return Ok(None);

View on GitHub (pinned to 73e0f67d83)