{"record":{"id":"9839c8cb076e2f96","repo":"astrid-runtime/astrid","slug":"capsule-disappeared-during-durable-scan","errorCode":null,"errorMessage":"capsule {} disappeared during durable scan","messagePattern":"capsule (.+?) disappeared during durable scan","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/meta.rs","lineNumber":158,"sourceCode":"    pub meta: Option<CapsuleMeta>,\n    /// Where this capsule was found.\n    pub location: CapsuleLocation,\n}\n\n/// Enumerate authoritative packages for one owner-root content projection.\n///\n/// This is the steady-state discovery API. It never scans native home or\n/// workspace capsule directories; those paths are migration/cache inputs only.\npub fn scan_durable_capsules(\n    store: &RuntimePrincipalStore,\n    owner: &StateOwner,\n    location: CapsuleLocation,\n) -> anyhow::Result<Vec<InstalledCapsule>> {\n    let registry = store.capsules();\n    let mut capsules = Vec::new();\n    for summary in registry.list(owner)? {\n        let Some(snapshot) = registry.get_snapshot(owner, summary.id())? else {\n            anyhow::bail!(\"capsule {} disappeared during durable scan\", summary.id());\n        };\n        let meta = serde_json::from_slice(&snapshot.package().metadata)\n            .with_context(|| format!(\"decode durable metadata for capsule {}\", summary.id()))?;\n        capsules.push(InstalledCapsule {\n            name: summary.id().to_owned(),\n            meta: Some(meta),\n            location,\n        });\n    }\n    capsules.sort_by(|left, right| left.name.cmp(&right.name));\n    Ok(capsules)\n}\n\n/// Scan user-level and workspace capsule directories, returning all installed\n/// capsules sorted alphabetically by name.\npub fn scan_installed_capsules() -> anyhow::Result<Vec<InstalledCapsule>> {\n    scan_installed_capsules_with_layout(&WorkspaceLayout::default())\n}","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/meta.rs#L140-L176","documentation":"scan_durable_capsules lists a principal's capsules from the durable registry and then fetches a full snapshot for each summary. If the snapshot lookup returns None for a summary that the same registry just listed, the registry is internally inconsistent (the entry vanished between list and get_snapshot), and the scan bails with this message rather than silently dropping the capsule.","triggerScenarios":"Concurrent deletion of a capsule (e.g. another process/thread calling remove on the RuntimePrincipalStore) between registry.list(owner) and registry.get_snapshot(owner, id); a corrupted or pruned store where snapshots were removed but summary index entries remain.","commonSituations":"Two tools managing capsules on the same principal concurrently; a crash or manual store cleanup that removed snapshot data but left summaries; filesystem-level deletion under the store while a scan runs.","solutions":["Re-run the scan; a transient concurrent delete may succeed on the second pass","Stop concurrent writers (other capsule installs/removes) during the scan","Inspect the principal store for orphaned summary entries and repair/remove them","If caused by external cleanup, restore capsule snapshots from backup or reinstall the capsule"],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"let capsules = loop {\n    match scan_durable_capsules(&store, owner, location).await {\n        Ok(c) => break c,\n        Err(e) if e.to_string().contains(\"disappeared during durable scan\") && retries < 3 => { retries += 1; continue; }\n        Err(e) => return Err(e),\n    }\n};","preventionTips":["Serialize capsule mutations (install/remove) with scans via a lock or the kernel","Avoid external tools deleting files under the principal store while scans run","Regularly audit the store for orphaned summary entries without snapshots","Use store-level transactions for remove so list/get_snapshot stay consistent"],"tags":["capsule-store","race-condition","internal-inconsistency"],"backgroundTag":"internal-invariant-violation","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"}