astrid-runtime/astrid · error

failed to unregister capsule

Error message

failed to unregister capsule '{id}': {e}

What it means

Thrown when `registry.unregister_for(principal, id)` fails with an error other than `CapsuleError::NotFound` during capsule uninstall (lib.rs:3153). NotFound is treated as a benign no-op (returns `Ok(false)`), but any other registry error — lock poisoning, invariant violation, unexpected internal state — is wrapped and propagated as a failed unregister.

Solutions

  1. Inspect the wrapped `{e}` cause (the `CapsuleError` variant) to see why the registry refused the removal
  2. Ensure no concurrent install/uninstall/restart is racing on the same capsule; serialize operations per capsule
  3. If the registry entry is stuck, reload or restart the kernel to rebuild registry state, then retry the uninstall
  4. Retry the uninstall: a transient state may clear once in-flight operations drain

Example fix

// before
let removed = kernel.unregister_capsule(&principal, &id).await?; // Err on non-NotFound
// after
match kernel.unregister_capsule(&principal, &id).await {
    Ok(removed) => tracing::info!(removed),
    Err(e) => {
        // inspect chained CapsuleError; serialize with other capsule ops and retry
        kernel.reload_registry_state().await?;
        kernel.unregister_capsule(&principal, &id).await?;
    },
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Only attempt unregister when the capsule is in a removable state
if !kernel.capsule_is_draining(principal, id).await {
    kernel.unregister_capsule(principal, id).await?;
}

Try / catch

match kernel.unregister_capsule(principal, id).await {
    Err(e) => {
        // inspect the chained CapsuleError variant; retry once after serializing
        serialize_on_capsule_load_lock(principal, id).await;
        kernel.unregister_capsule(principal, id).await.ok();
    },
    ok => ok?,
}

Prevention

When it happens

Trigger: Calling the kernel's uninstall/remove API where the registry write path returns a non-NotFound `CapsuleError` (lib.rs:3153), e.g. the registry rejects the removal due to inconsistent internal state or a guarded entry.

Common situations: Uninstalling a capsule while it is in a state the registry forbids removing; concurrent load/unload races despite the `capsule_load_lock`; internal registry corruption after a failed prior operation.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

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

    ///
    /// Returns an error only if the registry fails to unregister a capsule it
    /// reported as present.
    pub(crate) async fn unload_one_capsule(
        &self,
        id: &astrid_capsule_types::CapsuleId,
        principal: &PrincipalId,
    ) -> Result<bool, anyhow::Error> {
        let _view_guard = self.lock_capsule_view(principal, id).await;
        let load_guard = self.capsule_load_lock.lock().await;
        // A principal runtime is always torn down. An operator-owned
        // `SystemResident` runtime survives until its final view is released.
        let removed = {
            let mut registry = self.capsules.write().await;
            match registry.unregister_for(principal, id) {
                Ok(removed) => removed,
                Err(astrid_capsule_types::error::CapsuleError::NotFound(_)) => return Ok(false),
                Err(e) => {
                    return Err(anyhow::anyhow!("failed to unregister capsule '{id}': {e}"));
                },
            }
        };
        // Registration/reload is serialized by `capsule_load_lock`, so the
        // registry map lock can be released before awaiting the old runtime's
        // drain. This avoids a lock cycle with admitted host calls that perform
        // generation-scoped registry reads.
        if removed.torn_down {
            // The generation is no longer reachable from the registry. Close
            // every admission path before releasing the global publication
            // lock, then let generation-owned teardown drain independently.
            // This preserves hard MCP process-tree cleanup without allowing a
            // wedged child to block unrelated principals' lifecycle work.
            removed.capsule.retire();
            removed.capsule.request_cancel();
            drop(load_guard);
            removed.capsule.quiesce_for(principal).await;
        } else {

View on GitHub (pinned to affd8760f4)