astrid-runtime/astrid · error · io::Error
layout migration receipt does not match its intent or destin
Error message
layout migration receipt does not match its intent or destination: {} What it means
retire_verified_legacy_source cross-checks the completion receipt against the intent, the retirement record, and the destination: schema version, matching transaction ids, intent equality, recomputable source identity, and destination physical path must all agree. Any mismatch means the receipt does not certify the legacy state about to be deleted, so the library returns InvalidData instead of retiring state.db. The message includes the receipt path.
Source
Thrown at crates/astrid-core/src/dirs_layout.rs:375
let receipt: LayoutMigrationReceiptV1 = read_canonical_record(&receipt_path)?;
let intent: LayoutMigrationRecordV1 = read_canonical_record(&intent_path)?;
let retirement_path = self.migrations_dir().join(LAYOUT_MIGRATION_RETIREMENT);
let retirement: Option<LayoutRetirementV1> = match read_canonical_record(&retirement_path) {
Ok(retirement) => Some(retirement),
Err(error) if error.kind() == io::ErrorKind::NotFound => None,
Err(error) => return Err(error),
};
if receipt.schema != LAYOUT_MIGRATION_SCHEMA
|| receipt.transaction_id != intent.transaction_id
|| retirement.as_ref().is_some_and(|retirement| {
retirement.schema != LAYOUT_MIGRATION_SCHEMA
|| retirement.transaction_id != intent.transaction_id
})
|| receipt.intent != intent
|| !intent.has_recomputable_identity()
|| receipt.destination.physical_path_hex != intent.material.target_physical_path_hex
{
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"layout migration receipt does not match its intent or destination: {}",
receipt_path.display()
),
));
}
verify_receipt_destination_authority(&receipt.destination)?;
match std::fs::symlink_metadata(self.state_db_path()) {
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
Ok(_) => {
let source = inventory_tree(&self.state_db_path())?;
let expected = retirement.map_or_else(
|| receipt.intent.material.source.clone(),
|retirement| retirement.source,
);View on GitHub (pinned to affd8760f4)
Solutions
- Ensure var/migrations/ contains a coherent set: intent, retirement, and receipt all from the SAME migration transaction (matching transaction ids)
- If records are mixed or inconsistent, restore the full set from one consistent backup, or re-run the whole v1→v2 migration from legacy sentinel state
- Verify the astrid.volume file at its expected path is the same file the receipt recorded (its physical path hex must match the intent's target path)
- Do not hand-edit or regenerate individual migration records; they are content-bound and only valid as a matched set
Example fix
// before: intent and receipt from different transactions ls ~/.astrid/var/migrations/ # layout-v1-to-v2.intent (txn A), .complete (txn B) home.complete_layout_v2(&target)?; // InvalidData: receipt mismatch // after # restore all three records from the SAME backup, or: # reset sentinel to legacy and re-run the full migration so a matched set is written
Defensive patterns
Strategy: validation
Validate before calling
let (intent, receipt) = read_migration_records(&home)?;
if intent.transaction_id != receipt.transaction_id
|| receipt.intent != intent
|| receipt.destination.physical_path_hex != intent.material.target_physical_path_hex {
return Err(anyhow!("migration records are not a matched set"));
} Type guard
fn records_match(intent: &Intent, receipt: &Receipt) -> bool {
intent.transaction_id == receipt.transaction_id && receipt.intent == *intent
} Try / catch
match home.complete_layout_v2(&target) {
Err(e) if e.to_string().contains("does not match its intent or destination") => {
// restore the coherent record set from one backup, or re-run the migration
},
r => r?,
} Prevention
- Back up and restore intent/retirement/receipt always together, never piecemeal
- Don't re-run migrations after swapping individual record files
- Keep astrid.volume at the exact path the receipt recorded; relocate via library APIs only
- Log transaction ids at migration start to detect mixed-record states early
When it happens
Trigger: Calling complete_layout_v2 when var/migrations/ holds records from a different migration transaction (mixed old/new intent + receipt), the receipt was hand-edited or regenerated with different content, the destination astrid.volume was replaced or moved, or a schema-version mismatch exists between records.
Common situations: Re-running a migration after partially swapping var/migrations/ files; restoring receipt and intent from different backups; copying a home mid-migration so intent and receipt diverge; replacing astrid.volume after the receipt recorded its physical identity hash.
Understand the failure class
Background: Checksum mismatch errors: "checksum verification failed", "digest mismatch", "expected vs actual checksum" — what they mean and how to fix them — this error's family across 41 libraries.
Related errors
- layout-two home contains legacy state without a completion r
- legacy capsule authority identity differs for {id}
- durable capsule {id} failed byte-for-byte readback
- legacy capsule {id} authority verification produced no recei
- legacy capsule {id} authority receipt disappeared
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/b71157965ffe2099.
Report an issue: GitHub.