astrid-runtime/astrid · error

cannot reload capsule '{id}' for retiring principal '{princi

Error message

cannot reload capsule '{id}' for retiring principal '{principal}'

What it means

Reloading a capsule that is already registered to a principal is rejected if that principal is currently retiring. A reload would restart a runtime for an identity being torn down, so the kernel bails instead of calling `restart_capsule`.

Source

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

    ///
    /// If the capsule is already registered, [`Self::restart_capsule`] re-reads
    /// its source directory — picking up the new content-addressed bytes a
    /// reinstall wrote (a live upgrade / hot-swap). If it isn't registered yet,
    /// the currently-installed set is discovered and loaded (a fresh add;
    /// already-loaded capsules are skipped by `load_capsule`'s guard). Either
    /// way `astrid.v1.capsules_loaded` is published so the tool surface
    /// refreshes. Backs [`astrid_core::kernel_api::KernelRequest::ReloadCapsule`].
    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
    pub(crate) async fn reload_one_capsule(
        &self,
        id: &astrid_capsule_types::CapsuleId,
        principal: &PrincipalId,
    ) -> Result<(), anyhow::Error> {
        let view_guard = self.lock_capsule_view(principal, id).await;
        let registered = { self.capsules.read().await.get_for(principal, id).is_some() };
        if registered {
            if self.capabilities.is_principal_retiring(principal).await {
                anyhow::bail!("cannot reload capsule '{id}' for retiring principal '{principal}'");
            }
            self.restart_capsule(id, principal, None).await?;
            self.publish_capsules_loaded().await;
        } else {
            drop(view_guard);
            // Build or refresh this principal's view from its installed set.
            self.ensure_principal_loaded(principal).await;
            if self.capsules.read().await.get_for(principal, id).is_none()
                && let Some((_, dir)) = self
                    .sorted_principal_capsules(principal)
                    .into_iter()
                    .find(|(manifest, _)| manifest.package.name == id.as_str())
            {
                self.load_capsule(dir, principal)
                    .await
                    .map_err(|error| anyhow::anyhow!("capsule '{id}' failed to load: {error:#}"))?;
            }
            if self.capsules.read().await.get_for(principal, id).is_none() {

View on GitHub (pinned to affd8760f4)

Solutions

  1. Wait for retirement to finish (or cancel it) before reloading; the capsule will not need reloading if it is being torn down.
  2. Guard reload calls with an `is_principal_retiring` check and skip when true.
  3. Drop queued reload requests for principals marked retiring in your scheduler.

Example fix

// before
kernel.reload_capsule(id, &principal).await?;
// after
if !kernel.capabilities().is_principal_retiring(&principal).await {
    kernel.reload_capsule(id, &principal).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

if kernel.capabilities().is_principal_retiring(&principal).await {
    return Err(anyhow!("skip reload: principal retiring"));
}
if kernel.registered_for(&principal, id) {
    kernel.reload_capsule(id, &principal).await?;
}

Try / catch

match kernel.reload_capsule(id, &principal).await {
    Err(e) if e.to_string().contains("retiring principal") => cancel_pending_reload(id, principal),
    other => other,
}

Prevention

When it happens

Trigger: Calling the reload/restart API for an existing (registered) capsule while `capabilities.is_principal_retiring(principal)` returns true — e.g., a reload request arriving after a retire request was accepted.

Common situations: Config-change automation firing a reload while the same principal is being disabled; health-check-based reload loops colliding with operator retirement; retry queues replaying stale reload requests after retirement.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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