astrid-runtime/astrid · error

layout cutover record is not a regular file: {}

Error message

layout cutover record is not a regular file: {}

What it means

A completed layout v1-to-v2 migration must be backed by the durable cutover records `layout-v1-to-v2.intent` and `layout-v1-to-v2.complete` in the migrations directory. require_layout_provenance found one of these records missing (or reachable only through a redirect) and rejects the ledger with InvalidData. This prevents a fabricated or partially restored migrations directory from authorizing the layout finalizer.

Source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:203

            stack.push(path);
        }
    }
    Ok(targets)
}

/// A completed v2 ledger must be tied either to the explicit fresh-home
/// disposition or to the durable layout cutover intent and receipt.  This
/// check runs before stores open, so a canonical but invented component list
/// cannot authorize the legacy layout finalizer.
pub(super) fn require_layout_provenance(migrations: &Path, fresh_layout: bool) -> io::Result<()> {
    if fresh_layout {
        return Ok(());
    }
    for name in ["layout-v1-to-v2.intent", "layout-v1-to-v2.complete"] {
        let path = migrations.join(name);
        let metadata = fs::symlink_metadata(&path).map_err(|error| {
            if error.kind() == io::ErrorKind::NotFound {
                io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!(
                        "layout-two ledger has no durable cutover record: {}",
                        path.display()
                    ),
                )
            } else {
                error
            }
        })?;
        if metadata.file_type().is_symlink() || !metadata.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidData,
                format!(
                    "layout cutover record is not a regular file: {}",
                    path.display()
                ),
            ));

View on GitHub (pinned to affd8760f4)

Solutions

  1. Restore both `layout-v1-to-v2.intent` and `layout-v1-to-v2.complete` from a trusted backup of the migrations directory.
  2. If this home never completed the v1-to-v2 migration, complete or redo the migration so the records are written durably.
  3. Set the fresh-layout disposition explicitly if this is genuinely a brand-new home, so the records are not required.
  4. Verify you are pointing at the correct home directory; a wrong ASTRID_HOME looks identical to missing records.
  5. Never recreate the records by hand — they carry cryptographic provenance; hand-made copies will fail later digest checks.

Example fix

// before
mv ~/.astrid/migrations/layout-v1-to-v2.complete /tmp/  # hand-editing bookkeeping
// after
# restore from backup or rerun the migration to regenerate both records
cp backup/migrations/layout-v1-to-v2.* ~/.astrid/migrations/
Defensive patterns

Strategy: validation

Validate before calling

use std::fs;
fn cutover_records_present(migrations: &std::path::Path) -> std::io::Result<bool> {
    for name in ["layout-v1-to-v2.intent", "layout-v1-to-v2.complete"] {
        match fs::symlink_metadata(migrations.join(name)) {
            Ok(_) => {}
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(false),
            Err(e) => return Err(e),
        }
    }
    Ok(true)
}

Try / catch

if let Err(e) = require_layout_provenance(migrations, fresh_layout) {
    if e.to_string().contains("no durable cutover record") {
        // restore records from backup or rerun the migration
        return Err(MigrationError::MissingProvenance);
    }
    return Err(e.into());
}

Prevention

When it happens

Trigger: Calling reject_incomplete_layout_v2 / require_layout_provenance with fresh_layout=false when either `layout-v1-to-v2.intent` or `layout-v1-to-v2.complete` is absent under the migrations directory (NotFound is remapped here), or when symlink_metadata of the record path itself fails. The distinct 'not a regular file' variant at line 218 fires when the record exists but is a symlink or non-file.

Common situations: Restoring a home directory from a partial backup that omitted the migrations dir; users hand-deleting migration bookkeeping files; a crashed first run that never wrote both records; pointing the app at the wrong (fresh) home while a v2 ledger exists elsewhere.

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/9f443169e9b4f22c. Report an issue: GitHub.