astrid-runtime/astrid · error

remove durable capsule package '{id}': {error}

Error message

remove durable capsule package '{id}': {error}

What it means

Thrown in `remove_one_capsule` when `store.capsules().remove(&owner, id)` returns an `Err` after the live view was already unloaded. Before returning the error the kernel calls `ensure_principal_loaded(principal)` to restore the runtime view, since the durable package is still authoritative and must not be left unloaded.

Source

Thrown at crates/astrid-kernel/src/lib.rs:3273

            .uid_for(principal)
            .map_err(|error| anyhow::anyhow!("resolve durable owner for {principal}: {error}"))?;
        let owner = astrid_storage::StateOwner::Principal(uid);
        let snapshot = store
            .capsules()
            .get_snapshot(&owner, id.as_str())
            .map_err(|error| anyhow::anyhow!("read durable capsule package '{id}': {error}"))?;
        if snapshot.is_none() {
            return Ok(false);
        }
        // Quiesce and unload before deleting the durable package. If unload
        // fails, the package remains authoritative and can be retried on the
        // next request; no live runtime is left without its registry source.
        let _ = self.unload_one_capsule(id, principal).await?;
        let removed = match store.capsules().remove(&owner, id.as_str()) {
            Ok(removed) => removed,
            Err(error) => {
                self.ensure_principal_loaded(principal).await;
                return Err(anyhow::anyhow!(
                    "remove durable capsule package '{id}': {error}"
                ));
            },
        };
        if !removed {
            // A concurrent administrative writer won the generation race. The
            // durable package is still authoritative; restore the just-closed
            // runtime view before surfacing the conflict.
            self.ensure_principal_loaded(principal).await;
            return Err(anyhow::anyhow!(
                "durable capsule package '{id}' disappeared during removal"
            ));
        }
        Ok(true)
    }

    #[cfg(target_family = "wasm")]
    pub(crate) async fn remove_one_capsule(

View on GitHub (pinned to affd8760f4)

Solutions

  1. Read the wrapped backend error to identify the write failure; fix storage health/permissions and retry.
  2. Avoid concurrent admin mutations of the same capsule; serialize removals per principal.
  3. Verify the live view was restored (ensure_principal_loaded ran) before retrying, to avoid a runtime without its registry source.

Example fix

// before: remove while another admin task mutates the same capsule
join!(admin.remove(id), other.remove(id));
// after: serialize per principal
let _guard = removal_lock_for(&principal).lock().await;
admin.remove_one_capsule(&id, &principal).await?;
Defensive patterns

Strategy: retry

Validate before calling

fn store_writable(store: &Store) -> bool { !store.is_read_only() && store.health_check().is_ok() }

Try / catch

match kernel.remove_one_capsule(&id, &principal).await {
    Err(e) if is_transient_write_error(&e) => {
        // live view was restored by the kernel; safe to retry after backoff
        tokio::time::sleep(BACKOFF).await;
    },
    other => other?,
}

Prevention

When it happens

Trigger: The durable delete mutation fails: storage backend write error, generation check rejects the delete, lock contention, or the record changed state between the snapshot read and the remove call.

Common situations: Concurrent administrative operations on the same capsule; storage backend transient failure during write; read-only or degraded replica receiving the delete.

Related errors


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