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

  1. Call begin_layout_v2_migration first so the intent record is written, then complete_layout_v2.
  2. Point the completion call at the same directory that holds the record created during begin (check the path in the message).
  3. Restore the missing record from backup if begin already ran and the file was lost.
  4. 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

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


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)