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
- Reinstall or reload the capsule so a valid source directory is associated with its registry entry
- Restore the capsule source directory at the expected path, then retry the restart
- 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
- Never delete capsule source directories while entries remain registered
- For package-only capsules use the package redeploy path, not source-based restart
- Monitor install volumes/mounts so source paths cannot silently disappear
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
- an incomplete capsule authority update exists at
- audit fixture
- cannot load capsule ' ' for unadmitted principal
- cannot remove capsule authority while an install…
- capsule ' ' failed to load
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)