astrid-runtime/astrid · error

parse runtime tree receipt

Error message

parse runtime tree receipt: {error}

What it means

The runtime tree receipt file passed the size and type checks but its bytes are not valid JSON matching the expected receipt structure. The serde error is embedded in the message. This guards against corrupted or foreign files at the receipt path being interpreted as a valid receipt.

Solutions

  1. Delete the unreadable receipt and re-run admission so a fresh one is written (a missing receipt is treated as Ok(None))
  2. Read the embedded serde message to see the exact JSON path/type that failed
  3. Restore the receipt from backup if it carries state you need
  4. Ensure the app version reading the receipt matches the version that wrote it
Defensive patterns

Strategy: try-catch

Validate before calling

// cheap sanity check before handing the file to the kernel
if std::fs::read(&receipt_path)
    .map(|b| serde_json::from_slice::<serde_json::Value>(&b).is_ok())
    .unwrap_or(false) { /* proceed */ } else { std::fs::remove_file(&receipt_path)?; }

Try / catch

match err.downcast_ref::<io::Error>() {
    Some(e) if e.kind() == io::ErrorKind::InvalidData && msg.contains("parse runtime tree receipt") => {
        std::fs::remove_file(&path)?; // regenerate
        retry_admit()?;
    }
    _ => return Err(err),
}

Prevention

When it happens

Trigger: `read_receipt` runs `serde_json::from_slice(&bytes)` on the file contents after `fs::read`; parsing fails due to truncation, non-JSON content, or a receipt shape that doesn't deserialize into the expected type.

Common situations: Partial write from a crash; someone replaced the receipt with a different JSON file; schema drift between app versions where fields changed shape; corruption on disk.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/2fab857efdd87b59. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-kernel/src/runtime_tree_admit.rs:198

    if !metadata.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "runtime tree receipt is not a regular file: {}",
                path.display()
            ),
        ));
    }
    if metadata.len() > MAX_RECEIPT_BYTES {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("runtime tree receipt exceeds {MAX_RECEIPT_BYTES} bytes"),
        ));
    }
    astrid_core::platform_fs::validate_private_file(path)?;
    let bytes = fs::read(path)?;
    serde_json::from_slice(&bytes).map(Some).map_err(|error| {
        io::Error::new(
            io::ErrorKind::InvalidData,
            format!("parse runtime tree receipt: {error}"),
        )
    })
}

fn storage_error(error: impl std::error::Error + Send + Sync + 'static) -> io::Error {
    io::Error::other(error)
}

#[cfg(test)]
type SourceMutationHook = Box<dyn FnOnce(&Path) -> io::Result<()> + Send + 'static>;

#[cfg(test)]
static SOURCE_MUTATION_HOOK: std::sync::OnceLock<
    std::sync::Mutex<Option<(std::path::PathBuf, SourceMutationHook)>>,
> = std::sync::OnceLock::new();

View on GitHub (pinned to affd8760f4)