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
- 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.
- Close the race: ensure no other process manipulates the legacy path during migration.
- 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
- Freeze the legacy tree during migration so nothing swaps entries under the walker
- Audit the legacy path (`ls -ld`) before each migration attempt
- Avoid scripts that recreate legacy paths as symlinks during cleanup
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
- legacy source contains redirect or special entry: {}
- directory symlink {} not allowed in capsule source tree (ref
- legacy capsule directory is not a regular directory: {}
- legacy capsule entry is not a regular directory: {}
- capsule path escaped source root
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/987cdf835a48bb81.
Report an issue: GitHub.