astrid-runtime/astrid · error · io::Error
layout-two home contains legacy state without a completion…
Error message
layout-two home contains legacy state without a completion receipt
What it means
retire_verified_legacy_source (reached via complete_layout_v2 on an already-v2 home) found no completion receipt in var/migrations/, yet legacy state (state.db, cow/, or the migration intent) still exists. A v2 home without a receipt but with legacy leftovers is an inconsistent state the library will not clean up, because it cannot verify what the legacy state belongs to. Returned as InvalidData.
Solutions
- Restore var/migrations/ (especially layout-v1-to-v2.complete) from backup so the receipt again matches the surviving legacy state
- If the legacy state.db and cow/ are genuinely obsolete, verify the astrid.volume is healthy and then remove state.db, cow/, and the intent file manually — only after confirming the data was migrated
- Re-run the full v1→v2 migration from the legacy sentinel state instead of forcing retirement of an unverifiable home
- Do not delete receipt/intent files to retry migration; the retirement path is content-bound to them by design
Example fix
// before: receipt deleted but state.db remains
home.complete_layout_v2(&target)?; // InvalidData: legacy state without receipt
// after: restore the receipt, or remove verified-obsolete legacy state
std::fs::remove_file("~/.astrid/var/state.db")?; // only if data is confirmed migrated
std::fs::remove_dir_all("~/.astrid/var/cow")?;
home.complete_layout_v2(&target)?; Defensive patterns
Strategy: validation
Validate before calling
let receipt = home.migrations_dir().join("layout-v1-to-v2.complete");
let legacy_left = home.principal_store_path_parent_state_present(); // or check state.db/cow directly
if !receipt.exists() && (state_db.exists() || cow.exists()) {
return Err(anyhow!("v2 home has legacy state but no receipt; restore or clean first"));
} Type guard
fn retirement_is_safe(receipt: &Path, legacy: &[&Path]) -> bool {
receipt.is_file() || legacy.iter().all(|p| !p.exists())
} Try / catch
match home.complete_layout_v2(&target) {
Err(e) if e.to_string().contains("legacy state without a completion receipt") => {
// restore receipt from backup or manually remove verified-obsolete legacy state
},
r => r?,
} Prevention
- Never delete files under var/migrations/ to 'reset' migration state
- Restore backups as complete sets (etc/ + var/migrations/ together)
- Verify astrid.volume integrity before touching legacy state
- Let the normal startup/retirement path clean legacy state; avoid manual surgery
When it happens
Trigger: Calling complete_layout_v2 on a home whose sentinel already says v2, where var/migrations/layout-v1-to-v2.complete is missing but state.db, cow/, or layout-v1-to-v2.intent still exist — e.g. after a botched manual cleanup or restore that deleted the receipt but not the legacy data.
Common situations: Restoring a partial backup (receipt lost, legacy files kept); manually deleting files in var/migrations/ to 'reset' migration state; disk loss corrupting var/migrations/ while state.db survived; copying a half-migrated home.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- layout migration receipt does not match its intent or…
- legacy capsule authority verification produced no receipt
- component migration ledger is missing
- insufficient free space for layout migration: need
- invalid logical destination
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/6ad9f4d1ff8749da.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-core/src/dirs_layout.rs:337
self.state_db_path(),
];
for path in paths {
verify_existing_ancestor(&path)?;
}
validate_legacy_retirement_candidate(&self.cow_dir())?;
Ok(())
}
pub(super) fn retire_verified_legacy_source(&self) -> io::Result<()> {
let receipt_path = self.migrations_dir().join(LAYOUT_MIGRATION_RECEIPT);
let intent_path = self.migrations_dir().join(LAYOUT_MIGRATION_INTENT);
match std::fs::symlink_metadata(&receipt_path) {
Err(error) if error.kind() == io::ErrorKind::NotFound => {
if path_entry_present(&self.state_db_path())?
|| path_entry_present(&self.cow_dir())?
|| path_entry_present(&intent_path)?
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"layout-two home contains legacy state without a completion receipt",
));
}
return Ok(());
},
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"layout migration receipt is redirected or not a regular file: {}",
receipt_path.display()
),
));
},
Ok(_) => {},
Err(error) => return Err(error),
}View on GitHub (pinned to affd8760f4)