Hmbown/CodeWhale · error · io::Error

<dynamic: wrapped serde_json parse error>

Error message

<dynamic: wrapped serde_json parse error>

What it means

read_legacy_spillover_ownership reads a JSON sidecar describing legacy spillover ownership and deserializes it with serde_json. When the file's bytes are not valid JSON for the LegacySpilloverOwnership struct, the serde error is wrapped into an io::Error with ErrorKind::InvalidData. This surfaces as 'dynamic: wrapped serde_json parse error'.

Solutions

  1. Delete or fix the corrupt sidecar file so it parses as LegacySpilloverOwnership JSON
  2. Verify the sidecar contains a schema_version field matching LEGACY_SPILLOVER_OWNER_SCHEMA_VERSION
  3. Regenerate the sidecar by re-running the spillover migration/creation path
  4. Inspect the inner serde error message to identify the exact JSON field/type problem

Example fix

// before: hard failure on corrupt sidecar
let ownership = read_legacy_spillover_ownership(&sidecar)?;
// after: tolerate corrupt/absent legacy sidecar
let ownership = match read_legacy_spillover_ownership(&sidecar) {
    Ok(o) => Some(o),
    Err(e) if e.kind() == io::ErrorKind::InvalidData => None,
    Err(e) => return Err(e),
};
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check: is it plausible JSON with a schema_version?
let raw = std::fs::read_to_string(&sidecar)?;
if !raw.trim_start().starts_with('{') || !raw.contains("schema_version") {
    // treat as corrupt/legacy; recreate instead of calling the reader
}

Type guard

fn is_valid_sidecar(raw: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(raw)
        .map(|v| v.get("schema_version").is_some())
        .unwrap_or(false)
}

Try / catch

match read_legacy_spillover_ownership(&sidecar) {
    Ok(o) => o,
    Err(e) if e.kind() == io::ErrorKind::InvalidData => recreate_sidecar(&sidecar)?,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling read_legacy_spillover_ownership on a sidecar file whose contents are malformed JSON, missing required fields of LegacySpilloverOwnership (e.g. schema_version), or contain wrong-typed values.

Common situations: A partially-written or truncated sidecar from a crash; a sidecar hand-edited to invalid JSON; a schema drift where an older version wrote different fields; a non-JSON file at the sidecar path.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/tools/truncate.rs:172

    crate::utils::write_atomic(&sidecar, &encoded)?;
    Ok(sidecar)
}

pub(crate) fn read_legacy_spillover_ownership(
    payload_path: &Path,
) -> io::Result<LegacySpilloverOwnership> {
    let sidecar = legacy_spillover_ownership_path(payload_path);
    if std::fs::symlink_metadata(&sidecar)?
        .file_type()
        .is_symlink()
    {
        return Err(io::Error::new(
            io::ErrorKind::PermissionDenied,
            "legacy spillover ownership sidecar must not be a symlink",
        ));
    }
    let ownership = serde_json::from_slice::<LegacySpilloverOwnership>(&std::fs::read(sidecar)?)
        .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
    if ownership.schema_version != LEGACY_SPILLOVER_OWNER_SCHEMA_VERSION {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "unsupported legacy spillover ownership schema",
        ));
    }
    Ok(ownership)
}

/// Resolve the spillover-file path for a SHA256 content hash. Separate
/// namespace (`sha_<hex>.txt`) from the tool-call-id files so legacy
/// SHA-addressed evidence can be recognized without colliding with
/// tool-call references. Retrieval still requires matching ownership
/// metadata. `sha` must be the raw 64-char lowercase hex digest —
/// case-insensitive matching is done by the caller.
#[must_use]
pub fn sha_spillover_path(sha: &str) -> Option<PathBuf> {
    let sha = sha.trim().to_ascii_lowercase();

View on GitHub (pinned to 73e0f67d83)