astrid-runtime/astrid · error

legacy capsule metadata changed before retirement

Error message

legacy capsule {id} metadata changed before retirement

What it means

As a final pre-retirement check, migration re-reads meta.json and compares it byte-for-byte to the metadata that was published durably. A difference means the legacy metadata changed during migration, so the legacy directory is not retired and the error is raised to protect the mutated data.

Solutions

  1. Stop any process that could write meta.json, then re-run migration
  2. Serialize migrations so only one runs at a time
  3. Restore meta.json to the version matching the published package, or re-migrate from scratch

Example fix

// before
migrate while capsule service updates meta.json
// after
systemctl --user stop my-capsule; migrate_all_native_capsules(store)
Defensive patterns

Strategy: validation

Validate before calling

// verify meta.json stability before migrating
let before = std::fs::read(capsule_dir.join("meta.json")).map_err(|e| e.to_string())?;
std::thread::sleep(Duration::from_secs(1));
if std::fs::read(capsule_dir.join("meta.json")).map_err(|e| e.to_string())? != before {
    return Err("meta.json is being rewritten".into());
}

Try / catch

if let Err(e) = migrate_native_capsules(home, store) {
    if e.to_string().contains("metadata changed before retirement") {
        eprintln!("meta.json changed mid-migration; stop the writer and retry");
    } else { return Err(e); }
}

Prevention

When it happens

Trigger: meta.json was rewritten between its initial read and the final retirement check — e.g. the capsule process updated its own metadata, an installer touched it, or a sync tool restored a different copy.

Common situations: Running capsule writing version/state updates to meta.json mid-migration; backup/restore tools rewriting metadata; concurrent second migration attempt.

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


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

Appendix: source

Thrown at crates/astrid-capsule-install/src/storage/migration.rs:197

        };
        registry.install(&owner, id, &package, expectation)?;
        let readback = registry
            .get_snapshot(&owner, id)?
            .ok_or_else(|| anyhow::anyhow!("durable capsule {id} disappeared after publish"))?;
        if readback.package() != &package {
            bail!("durable capsule {id} failed byte-for-byte readback");
        }
        read_verified_durable_package_for_owner(store, &owner, id)?.ok_or_else(|| {
            anyhow::anyhow!("durable capsule {id} failed authoritative verification")
        })?;
        astrid_core::platform_fs::verify_no_redirects(&target)
            .with_context(|| format!("verify legacy capsule {id} before retirement"))?;
        let final_archive = canonical_legacy_archive(home, &target, &meta, &manifest)?;
        if final_archive != package.archive {
            bail!("legacy capsule {id} changed before retirement");
        }
        if fs::read(target.join("meta.json"))? != package.metadata {
            bail!("legacy capsule {id} metadata changed before retirement");
        }
        if read_installed_authority_bytes(home, &target)?.as_deref()
            != Some(source_authority_bytes.as_slice())
        {
            bail!("legacy capsule {id} authority changed before retirement");
        }
        astrid_core::platform_fs::verify_no_redirects(&target)
            .with_context(|| format!("verify legacy capsule {id} retirement boundary"))?;
        astrid_core::dirs::retire_legacy_source_tree(&target)
            .with_context(|| format!("retire migrated legacy capsule {id}"))?;
        retire_legacy_authority_receipt(home, &target, &source_authority_bytes)
            .with_context(|| format!("retire migrated legacy capsule {id} authority"))?;
        report
            .retired_authorities
            .push(LegacyCapsuleAuthorityReceipt {
                uid,
                capsule_id: id.to_owned(),
                authority_digest: blake3::hash(&package.authority).to_hex().to_string(),

View on GitHub (pinned to affd8760f4)