{"record":{"id":"5c94de8d4ae77b9c","repo":"ultraworkers/claw-code","slug":"lsp-registry-lock-poisoned","errorCode":null,"errorMessage":"lsp registry lock poisoned","messagePattern":"lsp registry lock poisoned","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"rust/crates/runtime/src/lsp_client.rs","lineNumber":197,"sourceCode":"\n    /// Add diagnostics to a server.\n    pub fn add_diagnostics(\n        &self,\n        language: &str,\n        diagnostics: Vec<LspDiagnostic>,\n    ) -> Result<(), String> {\n        let mut inner = self.inner.lock().expect(\"lsp registry lock poisoned\");\n        let server = inner\n            .servers\n            .get_mut(language)\n            .ok_or_else(|| format!(\"LSP server not found for language: {language}\"))?;\n        server.diagnostics.extend(diagnostics);\n        Ok(())\n    }\n\n    /// Get diagnostics for a specific file path.\n    pub fn get_diagnostics(&self, path: &str) -> Vec<LspDiagnostic> {\n        let inner = self.inner.lock().expect(\"lsp registry lock poisoned\");\n        inner\n            .servers\n            .values()\n            .flat_map(|s| &s.diagnostics)\n            .filter(|d| d.path == path)\n            .cloned()\n            .collect()\n    }\n\n    /// Clear diagnostics for a language server.\n    pub fn clear_diagnostics(&self, language: &str) -> Result<(), String> {\n        let mut inner = self.inner.lock().expect(\"lsp registry lock poisoned\");\n        let server = inner\n            .servers\n            .get_mut(language)\n            .ok_or_else(|| format!(\"LSP server not found for language: {language}\"))?;\n        server.diagnostics.clear();\n        Ok(())","sourceCodeStart":179,"sourceCodeEnd":215,"githubUrl":"https://github.com/ultraworkers/claw-code/blob/08106b0c3771ef5b4a5aa176acccd460e88b7325/rust/crates/runtime/src/lsp_client.rs#L179-L215","documentation":"Panic from the LSP registry's diagnostics-recording method (the push/record entry at lsp_client.rs:185-193): the std::sync::Mutex guarding the servers map is poisoned because some thread panicked while holding it, and .expect(\"lsp registry lock poisoned\") then unwinds in every subsequent caller. Rust mutexes poison on panic-by-holder, and the poison is sticky for the rest of the process — the registry is unusable until restart.","triggerScenarios":"Calling registry.record/push of diagnostics (extends server.diagnostics for a language) after any earlier panic occurred inside a critical section of this registry — e.g. a panic in server spawn, serialization of LspDiagnostic, or a .expect elsewhere in lsp_client.rs while the lock was held. Typical trigger order: thread A panics holding the lock in any method (dispatch, register, disconnect), thread B then calls this method and dies here.","commonSituations":"A malformed LSP server response causes an unwrap panic inside a locked section; a spawned language server crashes and the error path panics while the registry is locked; fuzz/load tests that intentionally panic in one thread and then reuse the shared LspRegistry in another.","solutions":["Find and fix the original panic — this expect is only the symptom; run with RUST_BACKTRACE=1 to see the first unwinding thread","Restart the process (or drop and recreate the LspRegistry) — poisoning cannot be cleared on a live Mutex","If you maintain this code, recover instead of dying: self.inner.lock().unwrap_or_else(|p| p.into_inner()) — the guarded data (a plain HashMap) has no broken invariants after a panic","Audit every panic site (unwrap/expect/indexing) reachable while the lock is held in lsp_client.rs and convert them to Result-returning errors"],"exampleFix":"// before (runtime/src/lsp_client.rs:186) — cascade panic on poison\nlet mut inner = self.inner.lock().expect(\"lsp registry lock poisoned\");\n\n// after — recover the (structurally valid) map from the PoisonError\nlet mut inner = self\n    .inner\n    .lock()\n    .unwrap_or_else(|poison| poison.into_inner());","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// record/push diagnostics can panic only via lock poisoning; contain it\nlet recorded = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {\n    registry.push_diagnostics(language, diagnostics.clone())\n}));\nif recorded.is_err() {\n    // registry lock is poisoned: rebuild the LspRegistry rather than reuse it\n    tracing::error!(\"lsp registry poisoned; recreating\");\n}","preventionTips":["Never allow panics (unwrap/expect/index slicing) inside code that holds the LSP registry lock — convert to Result errors","Parse language-server output outside the critical section; lock only for the map mutation","Treat one 'lsp registry lock poisoned' panic as fatal for that registry: rebuild it instead of calling more methods on it"],"tags":["panic","mutex","lock-poisoning","lsp","rust"],"backgroundTag":"mutex-lock-poisoned","analyzedSha":"08106b0c3771ef5b4a5aa176acccd460e88b7325","analyzedAt":"2026-08-18T00:29:38.590Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}