astrid-runtime/astrid · error

legacy source changed type

Error message

legacy source changed type: {}

What it means

This error is thrown by `read_regular_file` when a legacy source path that was expected to be a regular file no longer is. After opening the file with O_NOFOLLOW (so symlinks fail at open) and O_NONBLOCK, the code calls `file.metadata()` and rejects anything whose type is not a plain file. The library throws this to guarantee the legacy source being migrated cannot silently change identity between path validation and read.

Solutions

  1. Inspect the path reported in the message with `ls -la` and `stat` to see what it actually is.
  2. Remove or relocate the special entry so the legacy source contains only regular files.
  3. Re-run the migration; ensure no external process rewrites the legacy directory during migration.

Example fix

// before: path is a FIFO left by another tool
$ mkfifo ~/.legacy-app/state
// after: remove the special entry so migration sees a regular file
$ rm ~/.legacy-app/state
Defensive patterns

Strategy: validation

Validate before calling

let md = std::fs::symlink_metadata(path)?;
if !md.is_file() {
    return Err(anyhow!("skip migration: {} is not a regular file", path.display()));
}

Type guard

fn is_regular_file(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| m.is_file()).unwrap_or(false)
}

Try / catch

match astrid_kernel::read_regular_file(path) {
    Err(e) if e.to_string().contains("legacy source changed type") => {
        log::warn!("skipping special entry {}", path.display());
    }
    Err(e) => return Err(e.into()),
    Ok(data) => { /* proceed */ }
}

Prevention

When it happens

Trigger: Calling read_regular_file (via snapshot_path_with_access or snapshot_dir) on a path that is a FIFO, device, socket, or changed type after validation was performed.

Common situations: A special file (e.g. /dev entry or FIFO) is present in the legacy home directory; a file was replaced by a symlink or device node mid-migration; a security tool or another process mutated the legacy layout concurrently.

Understand the failure class

Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.

Related errors


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

Appendix: source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:817

        validate_private_entry(path, metadata)
    }
}

fn read_regular_file(
    path: &Path,
    hasher: &mut blake3::Hasher,
    identity: &mut SourceInventory,
) -> io::Result<()> {
    let mut options = OpenOptions::new();
    options.read(true);
    #[cfg(unix)]
    {
        use std::os::unix::fs::OpenOptionsExt as _;
        options.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_CLOEXEC | nix::libc::O_NONBLOCK);
    }
    let mut file = options.open(path)?;
    if !file.metadata()?.is_file() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy source changed type: {}", path.display()),
        ));
    }
    let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        identity.bytes = identity
            .bytes
            .checked_add(read as u64)
            .ok_or_else(|| io::Error::other("legacy source byte limit exceeded"))?;
        if identity.bytes.get() > MAX_BYTES {
            return Err(io::Error::other("legacy source byte limit exceeded"));
        }
        hasher.update(&buffer[..read]);

View on GitHub (pinned to affd8760f4)