Legacy env/secret migration is not idempotent: a failed boot permanently bricks the daemon
Summary
A daemon boot that fails after the legacy env/secret import has committed at least one scope can never boot again. The failure is permanent and self-inflicted: the import writes a receipt, retires the on-disk sources, and on the next boot recomputes a different receipt from the now-absent sources. The compare-and-swap then reports a conflict and the kernel aborts.
astrid start exits 1 forever with:
Error: Failed to boot Kernel: storage error: legacy import receipt conflicts with durable stateMechanism
write_import_receipt (crates/astrid-storage/src/env/legacy_import.rs:288) derives the receipt from the number of items it just moved:
let receipt = format!("legacy-import-v1 env={env_count} secrets={secret_count}");
match env_store
.compare_and_swap(LEGACY_IMPORT_MARKER_KEY, None, receipt.as_bytes().to_vec())On a re-run for an already-imported scope, the legacy files are gone, so env_count == 0 and secret_count == 0. The CAS returns Ok(false), the stored receipt does not equal the recomputed one, and the code hard-fails at legacy_import.rs:310:
Err(StorageError::Internal(
"legacy import receipt conflicts with durable state".to_owned(),
))The receipt encodes how much was moved rather than that the scope is done. Since retiring the sources is exactly what changes those counts, a successful import guarantees the next run disagrees with itself. The only reason this is not hit on every upgrade is that a boot which completes never re-runs the barrier.
This composes badly with #2 in this batch: that issue makes the barrier fail after several scopes have already imported, which is precisely the state that triggers this one.
Reproduction Steps
- Use an
~/.astridcreated by an older release that still has native env/secret scopes underhome/<principal>/.config/env/andsecrets/<principal>/. - Ensure the barrier will fail after at least one scope imports — e.g. leave a scope belonging to a principal that owns no capsules (see the companion issue), or interrupt the boot.
- Run
astrid start. It fails, but thedefaultprincipal's env/secret files are now gone and its receipt is committed. - Run
astrid startagain. - Observe
legacy import receipt conflicts with durable state. Every subsequent attempt fails identically.
Observed on my install: after step 3, home/default/.config/env/ and secrets/default/ were empty, and the daemon never booted again.
Expected Behavior
A partially completed migration should be resumable. Specifically:
- An already-imported scope should be recognised as complete and skipped, not re-imported and re-receipted.
- The receipt should be an idempotent completion marker (scope identity / schema version), not a count that the retirement step invalidates.
- Any re-derived value compared against a stored receipt must be computed from something the migration does not mutate.
At minimum, Ok(false) where the existing value is a well-formed legacy-import-v1 receipt for this scope should be treated as "already done", not as a conflict.
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) ~/.astridfirst created 2026-05-24; last healthy boot on 0.10.4- Legacy store format spec on disk:
PRE_FLEET_OWNER(9d701dc8…)
Logs / Backtrace
i Starting Astrid daemon (persistent mode)...
2026-09-15T06:35:48.301579Z INFO astrid_config::loader: loaded user config path=/Users/jamie/.astrid/config.toml
✗ error: Daemon exited prematurely (exit status: 1). Check logs: /Users/jamie/.astrid/log
# ~/.astrid/log/daemon-boot.log
Error: Failed to boot Kernel: storage error: legacy import receipt conflicts with durable stateState after the failing boot (sources retired, receipt committed, daemon down):
home/default/.config/env/ -> empty
secrets/default/ -> empty
home/claude-code/.config/env/ -> astrid-capsule-agents.env.json astrid-capsule-memory.env.json
astrid-capsule-openai-compat.env.json claude-runner.env.json
secrets/claude-code/ -> astrid-capsule-openai-compatSource: astrid-runtime/astrid