{"record":{"id":"99dd966fd9c192ff","repo":"astrid-runtime/astrid","slug":"legacy-source-changed-before-retirement","errorCode":null,"errorMessage":"legacy source changed before retirement: {}","messagePattern":"legacy source changed before retirement: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs","lineNumber":248,"sourceCode":"        }\n    }\n    Ok(())\n}\n\npub(super) fn retire_tree(\n    path: &Path,\n    expected: &SourceIdentity,\n    protected: &[PathBuf],\n) -> io::Result<()> {\n    let actual = snapshot_path(path)?;\n    if !actual.present {\n        // A prior post-ledger attempt completed its unlink before a crash.\n        // Absence is the idempotent terminal state regardless of whether the\n        // historical source identity was present.\n        return Ok(());\n    }\n    if &actual != expected {\n        return Err(io::Error::new(\n            io::ErrorKind::AlreadyExists,\n            format!(\n                \"legacy source changed before retirement: {}\",\n                path.display()\n            ),\n        ));\n    }\n    let metadata = fs::symlink_metadata(path)?;\n    if !metadata.is_dir() {\n        return Err(io::Error::new(\n            io::ErrorKind::InvalidData,\n            format!(\n                \"legacy retirement root is not a directory: {}\",\n                path.display()\n            ),\n        ));\n    }\n    if active_mountpoint(path)? {","sourceCodeStart":230,"sourceCodeEnd":266,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs#L230-L266","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Stop all processes that write into the legacy directory, then re-run migration; the snapshot will be retaken and match.","Identify what changed (compare against the ledger snapshot) and either revert the change or rerun preflight to record the new identity.","Ensure only one migration process runs at a time; use a lock if embedding the library.","Do not manually delete the tree to force retirement — use the supported migration path so provenance stays consistent."],"exampleFix":"# before\nsystemctl start legacy-astrid-daemon   # keeps writing during migration\n// after\nsystemctl stop legacy-astrid-daemon\nastrid migrate   # rerun; retirement now sees an unchanged tree","handlingStrategy":"try-catch","validationCode":"// Ensure no writer holds the legacy tree before migrating:\n// fuser -v <legacy-path>   (or lsof +D <legacy-path>) must return nothing.\nfn quiesced(path: &std::path::Path) -> bool {\n    std::fs::read_dir(path).map(|mut d| d.next().is_none() || external_lsof_clean(path)).unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"match retire_post_barrier_sources(...) {\n    Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists\n        && e.to_string().contains(\"changed before retirement\") => {\n        eprintln!(\"legacy tree mutated during migration; stop writers and re-run\");\n        // idempotent: safe to re-run the whole retirement\n    }\n    other => other?,\n}","preventionTips":["Stop legacy daemons/services before starting migration","Run only one migration instance at a time; use an exclusive lock","Suspend backup/sync tools (Dropbox, rsync cron) for the legacy directory during migration","Treat the error as retryable-after-quiescence, not as data loss — the tree is preserved"],"tags":["filesystem","migration","concurrency","rust"],"backgroundTag":"conflicting-config-options","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}