Migration barrier flattens anyhow chains, hiding the real boot failure cause
Summary
Boot failures in the legacy migration barrier are reported with {error} rather than {error:#}, so only the outermost anyhow context reaches the log. Every .with_context(...) frame underneath — which is where the actual cause lives — is discarded. Diagnosing a failed upgrade currently requires reading the source and inspecting on-disk state by hand.
Mechanism
crates/astrid-kernel/src/legacy_migration_barrier/mod.rs:345:
let capsule_report =
migrate_all_native_capsules_with_report(&Arc::new(store.clone()), home, directory)
.map_err(|error| {
io::Error::other(format!("legacy capsule migration failed: {error}"))
})?;anyhow::Error's Display prints only the top context. The producing code builds a deliberately informative chain — crates/astrid-capsule-install/src/storage/migration.rs:145:
verify_installed_authority(home, &target, &manifest)
.with_context(|| format!("verify legacy capsule authority {id}"))?;and verify_installed_authority (crates/astrid-capsule-install/src/authority.rs:533) bails with a message that names the exact discrepancy:
bail!(
"installed capsule identity/version differs from its authority receipt (approved {} {}, found {} {})",
authority.capsule_id, authority.version,
manifest.package.name, manifest.package.version
);None of that survives. What reaches daemon-boot.log is:
Error: Failed to boot Kernel: legacy capsule migration failed: verify legacy capsule authority astrid-capsule-agentsThat names the capsule but not the problem — it is indistinguishable from a missing WASM, a capability expansion, a manifest digest mismatch, or a schema version bump, all of which are separate bail!s in the same function.
To recover the real cause I had to read authority.rs, locate the receipt by grepping etc/capsule-authority/*.json for the capsule id, and compare it against the manifest by hand:
etc/capsule-authority/832eed9e….json -> "version": "0.1.0"
home/default/.local/capsules/astrid-capsule-agents/Capsule.toml -> version = "0.2.0"The receipt was stale — the capsule updated 0.1.0 → 0.2.0 on 2026-09-05 but the receipt still dated 2026-07-24. That one line of output would have been the whole diagnosis. Across my install, 8 of 19 capsules were in this state (agents, context-engine, fs, memory, prompt-builder, react, router, system).
Reproduction Steps
- Put any capsule into a state that trips a
bail!insideverify_installed_authority— e.g. update a capsule'sCapsule.tomlversion without refreshing its receipt underetc/capsule-authority/. - Run
astrid start. - Read
~/.astrid/log/daemon-boot.log. - Observe only
verify legacy capsule authority <id>, with no indication of which of the five failure modes occurred.
Expected Behavior
- Format
anyhowerrors crossing this boundary with the alternate selector ({error:#}) so the full context chain is preserved, or attach the source via a typed error rather than flattening to a string. - Apply the same treatment to the sibling
io::Error::other(format!(...))conversions in the barrier; this call site is the one I hit, not necessarily the only one. - Since these paths only run during upgrade, where the operator has no other signal, err toward more context rather than less.
Environment
- OS: macOS 15.6 (Darwin 24.6.0), arm64
- Astrid: 2026.9.2, installed via
astrid update - Source: astrid-runtime/astrid @
73661c9b(Cargo version 0.10.4) - 19 native capsules under
home/default/.local/capsules/, 8 with stale authority receipts
Logs / Backtrace
What was logged:
Error: Failed to boot Kernel: legacy capsule migration failed: verify legacy capsule authority astrid-capsule-agentsWhat the chain actually contained (recovered by hand):
legacy capsule migration failed
-> verify legacy capsule authority astrid-capsule-agents
-> installed capsule identity/version differs from its authority receipt
(approved astrid-capsule-agents 0.1.0, found astrid-capsule-agents 0.2.0)Source: astrid-runtime/astrid