astrid-runtime/astrid · error

remove stale capsule materialization

Error message

remove stale capsule materialization: {error}

What it means

Thrown by `repair_published_materialization` when the stale materialization directory passed the redirect check but `std::fs::remove_dir_all(target)` fails. The library needs to delete the stale cache directory before re-materializing from the package snapshot; if the OS refuses the delete (permissions, busy/open files, read-only filesystem), repair cannot proceed and the underlying io::Error is wrapped verbatim.

Solutions

  1. Delete the target directory manually with sufficient privileges (e.g. `sudo rm -rf <target>` or fixing ownership with `chown -R`), then re-run the operation.
  2. Close processes holding files open inside the target directory (dev servers, watchers, containers) and retry.
  3. If the cache is on a read-only mount, remount it read/write or relocate the cache directory to writable storage.
  4. Check and clear immutable attributes (`chattr -i`) or ACLs on the cache entries.
Defensive patterns

Strategy: validation

Validate before calling

use std::path::Path;

fn target_is_deletable(target: &Path) -> Result<(), String> {
    let md = std::fs::symlink_metadata(target)
        .map_err(|e| format!("target missing/unreadable: {e}"))?;
    if !md.is_dir() || md.file_type().is_symlink() {
        return Err("target is not a real directory".into());
    }
    // Probe write access to the directory (a directory can be removed only
    // if its parent is writable, and emptied only if children are writable).
    let probe = target.join(".astrid-delete-probe");
    std::fs::File::create(&probe)
        .and_then(|_| std::fs::remove_file(&probe))
        .map_err(|e| format!("cache dir not writable by current user: {e}"))
}

Type guard

fn can_write_directory(dir: &Path) -> bool {
    let probe = dir.join(".write-probe");
    let ok = std::fs::File::create(&probe).is_ok();
    let _ = std::fs::remove_file(&probe);
    ok
}

Try / catch

match ensure_published_materialization(&target, &principal, &manifest, &snapshot) {
    Ok(manifest) => {/* use bound manifest */}
    Err(e) if e.to_string().contains("remove stale capsule materialization") => {
        eprintln!("cache dir could not be deleted: {e:#}");
        eprintln!("hint: check ownership/permissions, close processes using it, or run: rm -rf {}");
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `ensure_published_materialization`/`capture_bound_materialization` where the target exists, is a real directory with a stale/unverifiable manifest, contains no redirects, but `remove_dir_all` returns an io::Error (e.g. EACCES, EBUSY, EROFS, EPERM on immutable files).

Common situations: Running the tool as a non-root user while the cache directory is owned by root (e.g. created by a sudo install); another process (editor, dev server, container) holds files open inside the cache on Windows/NFS; the cache lives on a read-only mount or Docker volume mounted `:ro`; file immutability flags (chattr +i) set on cache entries.

Understand the failure class

Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.

Related errors


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

Appendix: source

Thrown at crates/astrid-kernel/src/capsule_materialization.rs:200

            Err(error) => return Err(anyhow::anyhow!("inspect capsule materialization: {error}")),
        };
        if let Some(metadata) = target_metadata {
            if metadata.file_type().is_symlink() || !metadata.is_dir() {
                anyhow::bail!("capsule materialization target is redirected or not a directory");
            }
            if let Ok(bound_manifest) =
                astrid_capsule::discovery::load_manifest(&target.join("Capsule.toml"))
                && self
                    .verify_published_materialization(target, principal, &bound_manifest, snapshot)
                    .is_ok()
            {
                return Ok(bound_manifest);
            }
            astrid_core::platform_fs::verify_no_redirects(target).map_err(|error| {
                anyhow::anyhow!("capsule materialization target is redirected: {error}")
            })?;
            std::fs::remove_dir_all(target).map_err(|error| {
                anyhow::anyhow!("remove stale capsule materialization: {error}")
            })?;
        }
        astrid_capsule_install::materialize_capsule_package(snapshot.package(), target)
            .map_err(|error| anyhow::anyhow!("materialize durable capsule package: {error:#}"))?;
        let bound_manifest = astrid_capsule::discovery::load_manifest(&target.join("Capsule.toml"))
            .map_err(|error| anyhow::anyhow!(error))?;
        self.verify_published_materialization(target, principal, &bound_manifest, snapshot)?;
        Ok(bound_manifest)
    }

    /// Recheck the immutable publication after taking activation locks.
    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
    pub(crate) fn confirm_published_materialization(
        &self,
        dir: &Path,
        principal: &astrid_core::principal::PrincipalId,
        manifest: &astrid_capsule_types::manifest::CapsuleManifest,
        snapshot: &astrid_storage::CapsulePackageSnapshot,

View on GitHub (pinned to affd8760f4)