astrid-runtime/astrid · error · io::Error
layout migration source changed type: {}
Error message
layout migration source changed type: {} What it means
While inventorying the migration source, the library opens each regular file with no-follow flags (O_NOFOLLOW / FILE_FLAG_OPEN_REPARSE_POINT) and re-checks the opened file's metadata. If the opened handle is no longer a regular file (e.g. the entry was swapped for a symlink or fifo between the initial readdir stat and the open), it aborts instead of hashing unexpected content. This is a TOCTOU guard protecting the integrity of the migration inventory digest.
Source
Thrown at crates/astrid-core/src/dirs_layout_records.rs:414
hasher.update(&metadata.len().to_le_bytes());
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.custom_flags(nix::libc::O_NOFOLLOW | nix::libc::O_NONBLOCK);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt as _;
use windows_sys::Win32::Storage::FileSystem::FILE_FLAG_OPEN_REPARSE_POINT;
options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
let mut file = options.open(&child_path)?;
if !file.metadata()?.is_file() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"layout migration source changed type: {}",
child_path.display()
),
));
}
let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
let mut file_bytes = 0_u64;
loop {
let read = file.read(&mut buffer)?;
if read == 0 {
break;
}
*bytes = bytes
.checked_add(read as u64)
.ok_or_else(|| io::Error::other("layout inventory byte count overflow"))?;
file_bytes = file_bytesView on GitHub (pinned to affd8760f4)
Solutions
- Stop the process that is modifying the state directory and re-run the migration when the tree is quiescent.
- Check the reported path and restore it to a regular file with the expected content.
- Exclude the state directory from live-sync/backup tools, or pause them during migration.
- Retry the migration; the error is transient if it was caused by a concurrent swap.
Example fix
// before: rsync/backup daemon rewriting state files during migration * * * * * rsync -a --delete ~/live/ ~/.local/state/astrid/ // after: pause syncs while migrating systemctl --user stop backup.timer && astrid migrate && systemctl --user start backup.timer
Defensive patterns
Strategy: retry
Try / catch
match result {
Err(e) if e.to_string().contains("layout migration source changed type") => {
// pause writers/sync tools, then retry the migration
retry_with_backoff(|| run_migration(), 3);
}
Err(e) => return Err(e),
Ok(v) => v,
} Prevention
- Fully stop the application and any sync/backup daemons touching the state directory before migrating.
- Run migrations during maintenance windows when nothing writes to the tree.
- Exclude the state directory from live file-sync services.
When it happens
Trigger: inventory_directory opens a child with O_NOFOLLOW/REPARSE_POINT flags and file.metadata()?.is_file() is false — the entry changed from a regular file to something else (symlink, fifo, socket) between the directory scan and the open.
Common situations: Another process (backup, editor, sync daemon like Dropbox/Nextcloud) rewrites or replaces files mid-migration; a user manually deletes/recreates an entry as a symlink while a migration is running.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- legacy capsule {id} changed before retirement
- legacy capsule {id} metadata changed before retirement
- layout migration destination changed type: {}
- layout migration destination changed while inventoried: {}
- layout migration source changed while inventoried: {}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/e9045e26af095b63.
Report an issue: GitHub.