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
- Inspect the sidecar's JSON content (jq .) and the wrapped serde message to identify the mismatch.
- Delete the corrupt sidecar and let the owning session republish ownership via publish_legacy_spillover_ownership.
- If format changed between versions, migrate old sidecars or treat them as legacy and regenerate.
- 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
- Write sidecars only via write_atomic so readers never see partial JSON
- Do not hand-edit sidecar files
- Handle version skew by regenerating sidecars after upgrades
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
- <serde_json deserialization error>
- (serde_json deserialization error wrapped as…
- (serde_json serialization error wrapped as…
- deserialize
- <dynamic: wrapped serde_json serialization error>
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)