astrid-runtime/astrid · error

capsule ' ' has no source directory

Error message

capsule '{id}' has no source directory

What it means

Thrown during capsule restart when the registered capsule's `source_dir()` is `None` (lib.rs:2132). The restart procedure rebuilds the capsule from its on-disk source, so a registry entry without a source directory cannot be rebuilt and the operation is refused. This indicates a capsule that was registered without usable source material.

Solutions

  1. Reinstall or reload the capsule so a valid source directory is associated with its registry entry
  2. Restore the capsule source directory at the expected path, then retry the restart
  3. If the capsule is package-only by design, redeploy the package instead of using the source-based restart path

Example fix

// before
kernel.restart_capsule(&principal, &id, None).await?; // Err: no source directory
// after
if kernel.capsule_source_dir(&principal, &id).await.is_some() {
    kernel.restart_capsule(&principal, &id, None).await?;
} else {
    kernel.reinstall_capsule(&principal, &id).await?; // restore source
    kernel.restart_capsule(&principal, &id, None).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

let has_source = kernel.capsules()
    .read().await
    .get_for(principal, id)
    .and_then(|c| c.source_dir().map(|p| p.exists()))
    .unwrap_or(false);
if !has_source { /* reinstall before restart */ }

Type guard

fn has_source_dir(capsule: &CapsuleEntry) -> bool {
    capsule.source_dir().map(|p| p.exists()).unwrap_or(false)
}

Try / catch

match kernel.restart_capsule(principal, id, expected).await {
    Err(e) if e.to_string().contains("has no source directory") => {
        kernel.reinstall_capsule(principal, id).await?;
        kernel.restart_capsule(principal, id, expected).await?;
    },
    other => other?,
}

Prevention

When it happens

Trigger: Calling restart on a capsule whose registry entry has no source directory (lib.rs:2132) — e.g. a capsule loaded from a manifest-only/prepackaged snapshot without source, or source directory removed from disk and pruned from the entry.

Common situations: Capsules installed from durable packages rather than source directories being asked to restart; cleanup jobs deleting capsule source while entries remain registered; restarted deployments after volume/mount changes lost the source path.

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/15bd0fb6f524c910. Report an issue: GitHub.

Appendix: source

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

        id: &astrid_capsule_types::CapsuleId,
        principal: &PrincipalId,
        expected_runtime: Option<&astrid_capsule::registry::RuntimeId>,
    ) -> Result<RestartOutcome, anyhow::Error> {
        let (source_dir, current_runtime) = {
            let registry = self.capsules.read().await;
            let capsule = registry
                .get_for(principal, id)
                .ok_or_else(|| anyhow::anyhow!("capsule '{id}' not found in registry"))?;
            let runtime_id = registry
                .runtime_id_for(principal, id)
                .ok_or_else(|| anyhow::anyhow!("capsule '{id}' not found in registry"))?;
            if expected_runtime.is_some_and(|expected| expected != &runtime_id) {
                return Ok(RestartOutcome::Superseded);
            }
            let source_dir = capsule
                .source_dir()
                .map(std::path::Path::to_path_buf)
                .ok_or_else(|| anyhow::anyhow!("capsule '{id}' has no source directory"))?;
            (source_dir, runtime_id)
        };

        // Prepare and prove a route-gated replacement while the current
        // generation remains visible and healthy. A preparation or readiness
        // failure leaves the running view untouched.
        let mut prepared = self
            .prepare_runtime_replacement(id, &source_dir, principal, current_runtime.key().scope())
            .await?;

        let load_guard = self.capsule_load_lock.lock().await;
        if self.capabilities.is_principal_retiring(principal).await {
            drop(load_guard);
            prepared.capsule.retire();
            prepared.capsule.request_cancel();
            if let Err(cleanup) = prepared.capsule.unload().await {
                tracing::warn!(capsule_id = %id, %cleanup, "Failed to unload replacement rejected by principal retirement");
            }

View on GitHub (pinned to affd8760f4)