astrid-runtime/astrid · error · GatewayError::Internal
workspace capsule manifest changed while it was being read:
Error message
workspace capsule manifest changed while it was being read: {e} What it means
After parsing the capsule manifest, the handler re-resolves the same manifest path and compares it with the first resolution; if it changed, the manifest was mutated mid-read and the read is rejected as an Internal error. This TOCTOU guard ensures the returned env schema matches a consistent snapshot of the file.
Source
Thrown at crates/astrid-gateway/src/routes/env.rs:392
let Some(workspace_root) = workspace_root else {
return Err(GatewayError::NotFound);
};
let workspace = workspace_layout
.resolve(workspace_root)
.map_err(|e| GatewayError::Internal(anyhow::anyhow!("resolve selected workspace: {e}")))?;
let manifest_relative = FsPath::new("capsules")
.join(capsule_id)
.join("Capsule.toml");
let workspace_manifest = workspace.resolve_file(&manifest_relative).map_err(|e| {
GatewayError::Internal(anyhow::anyhow!("resolve workspace capsule manifest: {e}"))
})?;
if !workspace_manifest.exists() {
return Err(GatewayError::NotFound);
}
let schema = parse_env_schema(&workspace_manifest)?;
workspace.resolve_file(&manifest_relative).map_err(|e| {
GatewayError::Internal(anyhow::anyhow!(
"workspace capsule manifest changed while it was being read: {e}"
))
})?;
Ok(schema)
}
#[cfg(test)]
fn parse_env_schema(manifest_path: &FsPath) -> GatewayResult<HashMap<String, EnvFieldSchema>> {
let text = match std::fs::read_to_string(manifest_path) {
Ok(t) => t,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(GatewayError::NotFound);
},
Err(e) => {
return Err(GatewayError::Internal(anyhow::anyhow!(
"read {}: {e}",
manifest_path.display()
)));View on GitHub (pinned to affd8760f4)
Solutions
- Retry the env schema request — a one-shot concurrent write usually resolves on the next read
- Pause writers (build/CI jobs, editors with auto-save) that mutate Capsule.toml while the gateway serves reads
- Serialize manifest updates through the daemon so writes and gateway reads don't race
Example fix
// before
let schema = parse_env_schema(&workspace_manifest)?;
workspace.resolve_file(&manifest_relative).map_err(|e| {
GatewayError::Internal(anyhow::anyhow!(
"workspace capsule manifest changed while it was being read: {e}"
))
})?;
// after
let mut attempts = 0;
let schema = loop {
let result = (|| {
let workspace_manifest = workspace.resolve_file(&manifest_relative)?;
let schema = parse_env_schema(&workspace_manifest)?;
workspace.resolve_file(&manifest_relative)?;
Ok(schema)
})();
match result {
Ok(s) => break s,
Err(e) if attempts < 2 => { attempts += 1; continue; }
Err(e) => return Err(e),
}
}; Defensive patterns
Strategy: retry
Validate before calling
// Check manifest mtime stability before parsing
let before = fs::metadata(&manifest_path)?.modified()?;
std::thread::sleep(std::time::Duration::from_millis(50));
if fs::metadata(&manifest_path)?.modified()? != before {
return Err(anyhow::anyhow!("manifest is being written; retry later"));
} Try / catch
match load_env_schema_from_home(state, capsule_id).await {
Ok(schema) => schema,
Err(GatewayError::Internal(e))
if e.to_string().contains("changed while it was being read") =>
{
// transient TOCTOU race; safe to retry after a short backoff
tokio::time::sleep(Duration::from_millis(100)).await;
load_env_schema_from_home(state, capsule_id).await
}
Err(e) => return Err(e),
} Prevention
- Route all manifest writes through a single writer (the daemon) to avoid concurrent mutation
- Treat this error as transient and implement bounded retries with backoff
- Avoid editors/CI jobs that atomically rewrite Capsule.toml while the gateway is serving
When it happens
Trigger: The Capsule.toml at capsules/<id>/Capsule.toml is written, replaced, or renamed between the first resolve_file, the exists check, and the post-parse re-resolve during an env schema request.
Common situations: Another process (daemon, editor, CI job) rewriting Capsule.toml concurrently; atomic-replace via rename changing the inode; a container/CI environment where manifests are regenerated on the fly.
Understand the failure class
Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.
Related errors
- corpus input changed while its baseline snapshot was capture
- resolve selected workspace: {e}
- resolve workspace capsule manifest: {e}
- read capsule projection entry: {error}
- inspect capsule projection {}: {error}
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/8030f10bb7ddc138.
Report an issue: GitHub.