astrid-runtime/astrid · error

workspace commit for capsule

Error message

workspace commit for capsule '{id}' failed: {e}

What it means

Thrown in `commit_workspace_for` when the capsule's copy-on-write workspace promote (commit) or rollback fails. The kernel wraps the underlying workspace error with the capsule id; `Ok(None)` means the capsule was not loaded, so this error only fires for a loaded capsule whose OS-level CoW workspace operation failed.

Solutions

  1. Inspect the wrapped source error for the filesystem-level cause (permissions, missing path, busy files).
  2. Close processes holding files in the capsule workspace, then retry the promote/rollback.
  3. If the workspace is inconsistent, reset the workspace (or re-load the capsule) and re-run the gate decision.

Example fix

// before: lose the capsule id context
outcome?;
// after: ensure workspace paths are writable and processes closed before commit
for path in workspace_paths(&capsule) {
    let meta = std::fs::metadata(&path)
        .expect("workspace path must exist before commit");
    assert!(!meta.permissions().readonly(), "workspace must be writable");
}
kernel.commit_workspace_for(&id, &principal, true).await?;
Defensive patterns

Strategy: try-catch

Validate before calling

fn workspace_ready(capsule: &Capsule) -> bool {
    capsule.workspace_paths().iter().all(|p| p.exists() && !p.metadata().unwrap().permissions().readonly())
}

Try / catch

match kernel.commit_workspace_for(&id, &principal, true).await {
    Ok(None) => info!("capsule not loaded; nothing to commit"),
    Ok(Some(_)) => info!("workspace committed"),
    Err(e) if e.to_string().contains("workspace commit") => {
        error!("CoW workspace op failed: {e:#}; reload capsule and retry");
    },
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Gate approve/reject invokes `commit_workspace_for` and `capsule.promote_workspace` or `capsule.rollback_workspace` errors: filesystem failure during the copy-on-write merge/discard (permissions, missing paths, files held open), or the workspace snapshot is in an inconsistent state.

Common situations: Files inside the capsule workspace locked by another process at promote time; read-only volume; workspace directory manually altered or deleted between snapshot and commit; disk exhaustion during copy-on-write materialization.

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/61bebb7d736505da. Report an issue: GitHub.

Appendix: source

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

    /// (git-managed or No-CoW — nothing to do).
    pub(crate) async fn commit_workspace_for(
        &self,
        id: &astrid_capsule_types::CapsuleId,
        principal: &PrincipalId,
        commit: bool,
    ) -> Result<Option<bool>, anyhow::Error> {
        let capsule = { self.capsules.read().await.get_for(principal, id) };
        let Some(capsule) = capsule else {
            return Ok(None);
        };
        let outcome = if commit {
            capsule.promote_workspace(principal).await
        } else {
            capsule.rollback_workspace(principal).await
        };
        outcome
            .map(Some)
            .map_err(|e| anyhow::anyhow!("workspace commit for capsule '{id}' failed: {e}"))
    }

    /// Record that a new client connection for `principal` has been established.
    pub fn connection_opened(&self, principal: &PrincipalId) {
        self.active_connections
            .entry(principal.clone())
            .or_insert_with(|| AtomicUsize::new(0))
            .fetch_add(1, Ordering::Relaxed);
        metrics::counter!(METRIC_CONNECTIONS_OPENED_TOTAL).increment(1);
        metrics::gauge!(METRIC_ACTIVE_CONNECTIONS).increment(1.0);
    }

    /// Record that a client connection for `principal` has been closed.
    ///
    /// Uses `fetch_update` for atomic saturating decrement - avoids the
    /// TOCTOU window where `fetch_sub` wraps to `usize::MAX` before a
    /// corrective store.
    ///

View on GitHub (pinned to affd8760f4)