astrid-runtime/astrid · error · io::Error

layout migration record does not match this transaction

Error message

layout migration record does not match this transaction: {}

What it means

After successfully parsing the record, admit_or_write_canonical compares it to the expected transaction record, both by value and byte-for-byte (including the trailing newline produced by canonical serialization). Any mismatch means the file on disk belongs to a different migration/transaction than the one being executed, so the library aborts with InvalidData rather than mixing transactions.

Solutions

  1. Inspect the record file at the reported path to see which transaction it belongs to; complete or roll back that transaction first.
  2. Remove the stale/foreign record and re-run the current migration cleanly.
  3. Ensure migrations are not run concurrently against the same layout directory (add locking in the caller).
  4. Never reformat or regenerate the JSON manually — records must be byte-canonical as the library writes them.

Example fix

// before: pretty-printed record breaks canonical-byte check
{ "version": 2 }
// after: let the library write it
std::fs::remove_file(record_path)?;
begin_layout_v2_migration(&dir)?; // rewrites canonical bytes
Defensive patterns

Strategy: validation

Validate before calling

// verify the on-disk record matches this transaction before completing
let bytes = std::fs::read(record_path)?;
let expected = serde_json::to_vec(&expected_record)?;
if bytes != expected { return Err("record belongs to another transaction"); }

Try / catch

match result {
    Err(e) if e.to_string().contains("does not match this transaction") => {
        // inspect the file, finish/roll back the other transaction, then retry
    },
    other => other?,
}

Prevention

When it happens

Trigger: begin_layout_v2_migration or complete_layout_v2 finds an existing record whose parsed value differs from `expected`, or whose raw bytes are not the canonical serialization of the expected record (extra whitespace, missing trailing newline, different fields/timestamps).

Common situations: Two concurrent or interleaved migrations started with different parameters writing the same record path; a stale record left from an aborted earlier migration attempt; a tool reformatted (pretty-printed) the JSON file, breaking byte-canonicality.

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


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

Appendix: source

Thrown at crates/astrid-core/src/dirs_layout_records.rs:128

) -> io::Result<()>
where
    T: DeserializeOwned + PartialEq + Serialize,
{
    let mut expected_bytes = serde_json::to_vec(expected).map_err(io::Error::other)?;
    expected_bytes.push(b'\n');
    match std::fs::read(path) {
        Ok(actual) => {
            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()
                    ),
                )
            })?;
            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),
    }

View on GitHub (pinned to affd8760f4)