astrid-runtime/astrid · error · io::Error
layout migration intent is missing
Error message
layout migration intent is missing: {} What it means
admit_or_write_canonical has two modes: allow_create (write the expected record atomically) and strict mode where the record must already exist. In strict mode, if the record file is not found (io::ErrorKind::NotFound), it raises InvalidData "layout migration intent is missing: {path}" — the caller is trying to continue or complete a migration whose begin/intent record was never written or has been deleted.
Solutions
- Call begin_layout_v2_migration first so the intent record is written, then complete_layout_v2.
- Point the completion call at the same directory that holds the record created during begin (check the path in the message).
- Restore the missing record from backup if begin already ran and the file was lost.
- Do not delete files under the layout directory between begin and complete.
Example fix
// before complete_layout_v2(&dir)?; // record never written // after begin_layout_v2_migration(&dir)?; complete_layout_v2(&dir)?;
Defensive patterns
Strategy: validation
Validate before calling
if !record_path.exists() {
return Err(format!("migration intent record missing at {} — run begin first", record_path.display()));
} Try / catch
match complete_layout_v2(&dir) {
Err(e) if e.kind() == io::ErrorKind::InvalidData && e.to_string().contains("intent is missing") => {
begin_layout_v2_migration(&dir)?;
complete_layout_v2(&dir)?;
},
other => other?,
} Prevention
- Always pair begin_layout_v2_migration with complete_layout_v2 on the same directory
- Exclude layout record files from cleanup/backup-restore jobs
- Verify record_path.exists() before calling completion APIs
When it happens
Trigger: complete_layout_v2 (or another strict caller) runs while the intent/record file at `path` does not exist, i.e. the NotFound arm with allow_create == false is taken.
Common situations: Calling complete_layout_v2 without ever calling begin_layout_v2_migration; the record file was deleted by cleanup scripts or the user; running the completion step on a different data directory than the one the migration began in.
Understand the failure class
Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.
Related errors
- component migration ledger is missing
- historical principal home receipt is missing or changed
- absent migration source has a digest
- automatic migration of unreleased Windows layout-one homes…
- cannot migrate an Astrid home without a layout-version…
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/90c2e0d615a07998.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-core/src/dirs_layout_records.rs:141
path.display()
),
)
})?;
if parsed != *expected || actual != expected_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"layout migration record does not match this transaction: {}",
path.display()
),
));
}
Ok(())
},
Err(error) if error.kind() == io::ErrorKind::NotFound && allow_create => {
super::atomic_write(path, &expected_bytes)
},
Err(error) if error.kind() == io::ErrorKind::NotFound => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("layout migration intent is missing: {}", path.display()),
)),
Err(error) => Err(error),
}
}
pub(super) fn read_canonical_record<T>(path: &Path) -> io::Result<T>
where
T: DeserializeOwned + PartialEq + Serialize,
{
let actual = std::fs::read(path)?;
let parsed: T = serde_json::from_slice(&actual).map_err(|error| {
io::Error::new(
io::ErrorKind::InvalidData,
format!(
"invalid layout migration record {}: {error}",
path.display()View on GitHub (pinned to affd8760f4)