astrid-runtime/astrid · error

legacy retirement root is not a directory: {}

Error message

legacy retirement root is not a directory: {}

What it means

After the identity check passes, retire_tree verifies via symlink_metadata that the retirement root is actually a directory. If it is a symlink, regular file, or special node, deletion is refused with InvalidData. Retirement only ever removes real directories bottom-up.

Source

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

    let actual = snapshot_path(path)?;
    if !actual.present {
        // A prior post-ledger attempt completed its unlink before a crash.
        // Absence is the idempotent terminal state regardless of whether the
        // historical source identity was present.
        return Ok(());
    }
    if &actual != expected {
        return Err(io::Error::new(
            io::ErrorKind::AlreadyExists,
            format!(
                "legacy source changed before retirement: {}",
                path.display()
            ),
        ));
    }
    let metadata = fs::symlink_metadata(path)?;
    if !metadata.is_dir() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!(
                "legacy retirement root is not a directory: {}",
                path.display()
            ),
        ));
    }
    if active_mountpoint(path)? {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("legacy source is an active mount: {}", path.display()),
        ));
    }
    let device = device_id(&metadata);
    for entry in fs::read_dir(path).map_err(io::Error::other)? {
        let child = entry.map_err(io::Error::other)?.path();
        if protected.iter().any(|candidate| candidate == &child) {
            return Err(io::Error::new(

View on GitHub (pinned to affd8760f4)

Solutions

  1. Inspect the path (`ls -ld`) and restore it to the real directory expected by the ledger, or remove the foreign entry and rerun preflight so the ledger matches reality.
  2. Close the race: ensure no other process manipulates the legacy path during migration.
  3. If the source genuinely is a file, use the file-retirement path rather than retire_tree.

Example fix

// before
ln -s /elsewhere/old-capsule ~/.astrid/legacy/capsule
// after
rm ~/.astrid/legacy/capsule
mv /elsewhere/old-capsule ~/.astrid/legacy/capsule   # real directory in place
Defensive patterns

Strategy: validation

Validate before calling

use std::fs;
fn retirement_root_ok(path: &std::path::Path) -> std::io::Result<bool> {
    let m = fs::symlink_metadata(path)?;
    Ok(!m.file_type().is_symlink() && m.is_dir())
}

Type guard

fn is_real_dir(m: &std::fs::Metadata) -> bool {
    !m.file_type().is_symlink() && m.is_dir()
}

Try / catch

match retire_tree(path, &expected, &protected) {
    Err(e) if e.to_string().contains("retirement root is not a directory") => {
        eprintln!("{} was replaced by a non-directory; restore before migrating", path.display());
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling retire_tree when the path recorded in the ledger was replaced by a symlink or file after the snapshot — e.g. the snapshot said 'present' matching a directory identity, but by the time symlink_metadata runs, the entry is not a directory, or the identity check passed for a file-typed source and retire_tree is invoked on it.

Common situations: A race where another process swapped the directory for a symlink between snapshot and retirement; configuration pointing retirement at a file path; restore tooling recreating the legacy path as a link.

Related errors


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