Hmbown/CodeWhale · error · std::io::Error

(serde_json deserialization error wrapped as…

Error message

(serde_json deserialization error wrapped as io::ErrorKind::InvalidData)

What it means

read_legacy_spillover_ownership parses the ownership sidecar JSON into LegacySpilloverOwnership. If the bytes are not valid JSON or do not match the struct's schema, serde's error is wrapped as io::ErrorKind::InvalidData. The sidecar passed the symlink check, so this is purely a content problem: corrupt, truncated, or schema-incompatible data.

Solutions

  1. Inspect the sidecar's JSON content (jq .) and the wrapped serde message to identify the mismatch.
  2. Delete the corrupt sidecar and let the owning session republish ownership via publish_legacy_spillover_ownership.
  3. If format changed between versions, migrate old sidecars or treat them as legacy and regenerate.
  4. Guard callers so InvalidData means 'unowned/corrupt payload' and triggers the legacy-recovery path instead of aborting.

Example fix

// before
let ownership = read_legacy_spillover_ownership(&payload_path)?;
// after
let ownership = read_legacy_spillover_ownership(&payload_path)
    .or_else(|e| if e.kind() == std::io::ErrorKind::InvalidData {
        republish_ownership(&payload_path) // regenerate sidecar
    } else { Err(e) })?;
Defensive patterns

Strategy: try-catch

Validate before calling

fn sidecar_parses(p: &std::path::Path) -> bool {
    std::fs::read(p).map(|b| serde_json::from_slice::<serde_json::Value>(&b).is_ok()).unwrap_or(false)
}

Type guard

fn parses_as_ownership(raw: &[u8]) -> bool {
    serde_json::from_slice::<crate::tools::LegacySpilloverOwnership>(raw).is_ok()
}

Try / catch

match read_legacy_spillover_ownership(&payload_path) {
    Err(e) if e.kind() == std::io::ErrorKind::InvalidData => republish_ownership(&payload_path),
    other => other?,
}

Prevention

When it happens

Trigger: Calling read_legacy_spillover_ownership when the sidecar exists and is a regular file but its contents fail serde_json::from_slice::<LegacySpilloverOwnership> — missing fields, wrong types, trailing garbage, or empty file.

Common situations: Interrupted write of the sidecar (crash before write_atomic completed); manual edits; version skew where the sidecar format changed; an unrelated file substituted at the sidecar path.

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@433685b202 (2026-09-15). Data as JSON: /api/errors/6bb5e560a886871c. 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 433685b202)