#1941·astrid

Legacy env/secret migration is not idempotent: a failed boot permanently bricks the daemon

Author: jvsteinerCreated Sep 15, 2026Updated Sep 15, 2026

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 state

Mechanism

write_import_receipt (crates/astrid-storage/src/env/legacy_import.rs:288) derives the receipt from the number of items it just moved:

rust
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:

rust
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

  1. Use an ~/.astrid created by an older release that still has native env/secret scopes under home/<principal>/.config/env/ and secrets/<principal>/.
  2. 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.
  3. Run astrid start. It fails, but the default principal's env/secret files are now gone and its receipt is committed.
  4. Run astrid start again.
  5. 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)
  • ~/.astrid first 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 state

State 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-compat