{"record":{"id":"8030f10bb7ddc138","repo":"astrid-runtime/astrid","slug":"workspace-capsule-manifest-changed-while-it-was-be","errorCode":null,"errorMessage":"workspace capsule manifest changed while it was being read: {e}","messagePattern":"workspace capsule manifest changed while it was being read: (.+?)","errorType":"http","errorClass":"GatewayError::Internal","httpStatus":500,"severity":"error","filePath":"crates/astrid-gateway/src/routes/env.rs","lineNumber":392,"sourceCode":"\n    let Some(workspace_root) = workspace_root else {\n        return Err(GatewayError::NotFound);\n    };\n    let workspace = workspace_layout\n        .resolve(workspace_root)\n        .map_err(|e| GatewayError::Internal(anyhow::anyhow!(\"resolve selected workspace: {e}\")))?;\n    let manifest_relative = FsPath::new(\"capsules\")\n        .join(capsule_id)\n        .join(\"Capsule.toml\");\n    let workspace_manifest = workspace.resolve_file(&manifest_relative).map_err(|e| {\n        GatewayError::Internal(anyhow::anyhow!(\"resolve workspace capsule manifest: {e}\"))\n    })?;\n    if !workspace_manifest.exists() {\n        return Err(GatewayError::NotFound);\n    }\n    let schema = parse_env_schema(&workspace_manifest)?;\n    workspace.resolve_file(&manifest_relative).map_err(|e| {\n        GatewayError::Internal(anyhow::anyhow!(\n            \"workspace capsule manifest changed while it was being read: {e}\"\n        ))\n    })?;\n    Ok(schema)\n}\n\n#[cfg(test)]\nfn parse_env_schema(manifest_path: &FsPath) -> GatewayResult<HashMap<String, EnvFieldSchema>> {\n    let text = match std::fs::read_to_string(manifest_path) {\n        Ok(t) => t,\n        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {\n            return Err(GatewayError::NotFound);\n        },\n        Err(e) => {\n            return Err(GatewayError::Internal(anyhow::anyhow!(\n                \"read {}: {e}\",\n                manifest_path.display()\n            )));","sourceCodeStart":374,"sourceCodeEnd":410,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/routes/env.rs#L374-L410","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet schema = parse_env_schema(&workspace_manifest)?;\nworkspace.resolve_file(&manifest_relative).map_err(|e| {\n    GatewayError::Internal(anyhow::anyhow!(\n        \"workspace capsule manifest changed while it was being read: {e}\"\n    ))\n})?;\n// after\nlet mut attempts = 0;\nlet schema = loop {\n    let result = (|| {\n        let workspace_manifest = workspace.resolve_file(&manifest_relative)?;\n        let schema = parse_env_schema(&workspace_manifest)?;\n        workspace.resolve_file(&manifest_relative)?;\n        Ok(schema)\n    })();\n    match result {\n        Ok(s) => break s,\n        Err(e) if attempts < 2 => { attempts += 1; continue; }\n        Err(e) => return Err(e),\n    }\n};","handlingStrategy":"retry","validationCode":"// Check manifest mtime stability before parsing\nlet before = fs::metadata(&manifest_path)?.modified()?;\nstd::thread::sleep(std::time::Duration::from_millis(50));\nif fs::metadata(&manifest_path)?.modified()? != before {\n    return Err(anyhow::anyhow!(\"manifest is being written; retry later\"));\n}","typeGuard":null,"tryCatchPattern":"match load_env_schema_from_home(state, capsule_id).await {\n    Ok(schema) => schema,\n    Err(GatewayError::Internal(e))\n        if e.to_string().contains(\"changed while it was being read\") =>\n    {\n        // transient TOCTOU race; safe to retry after a short backoff\n        tokio::time::sleep(Duration::from_millis(100)).await;\n        load_env_schema_from_home(state, capsule_id).await\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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"],"tags":["rust","concurrency","race-condition","filesystem","gateway"],"backgroundTag":"file-read-failed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}