astrid-runtime/astrid · error

legacy source changed before retirement: {}

Error message

legacy source changed before retirement: {}

What it means

During retirement of a legacy source tree, retire_tree re-snapshots the path and compares it to the SourceIdentity recorded in the preflight ledger. If the tree still exists but its digest/entry-count/byte-count no longer match the snapshot — or a leaf's size/snapshot differs just before unlink — the library refuses to delete it and raises AlreadyExists. This is a data-safety abort: changed data must not be swept by migration.

Source

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

        }
    }
    Ok(())
}

pub(super) fn retire_tree(
    path: &Path,
    expected: &SourceIdentity,
    protected: &[PathBuf],
) -> io::Result<()> {
    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)? {

View on GitHub (pinned to affd8760f4)

Solutions

  1. Stop all processes that write into the legacy directory, then re-run migration; the snapshot will be retaken and match.
  2. Identify what changed (compare against the ledger snapshot) and either revert the change or rerun preflight to record the new identity.
  3. Ensure only one migration process runs at a time; use a lock if embedding the library.
  4. Do not manually delete the tree to force retirement — use the supported migration path so provenance stays consistent.

Example fix

# before
systemctl start legacy-astrid-daemon   # keeps writing during migration
// after
systemctl stop legacy-astrid-daemon
astrid migrate   # rerun; retirement now sees an unchanged tree
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure no writer holds the legacy tree before migrating:
// fuser -v <legacy-path>   (or lsof +D <legacy-path>) must return nothing.
fn quiesced(path: &std::path::Path) -> bool {
    std::fs::read_dir(path).map(|mut d| d.next().is_none() || external_lsof_clean(path)).unwrap_or(false)
}

Try / catch

match retire_post_barrier_sources(...) {
    Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists
        && e.to_string().contains("changed before retirement") => {
        eprintln!("legacy tree mutated during migration; stop writers and re-run");
        // idempotent: safe to re-run the whole retirement
    }
    other => other?,
}

Prevention

When it happens

Trigger: Any mutation of the legacy tree between preflight snapshot and retirement: files added, removed, or modified (digest or byte mismatch), a leaf file's size changing between retire_tree's leaf check and retire_leaf, or the path being replaced by different content. Raised recursively for every child via retire_tree and retire_leaf. Callers include retire_post_barrier_sources and the retirement tests.

Common situations: A running legacy daemon still writing logs/state into its old directory during migration; the user editing files in the legacy location mid-upgrade; a background sync/backup tool touching mtimes and content; concurrent second migration instance.

Related errors


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