From c77ef47cfc2b4ae65e5dc15df4204a71bf574fca Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Sat, 25 Jul 2026 13:12:49 +0200 Subject: [PATCH] Recover poisoned shared-state locks --- src/app/preferences.rs | 43 +++++++++++++++++++++++++++++++++++++++--- src/server/tools.rs | 43 +++++++++++++++++++++++------------------- 2 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/app/preferences.rs b/src/app/preferences.rs index 0c307ce..f049a7a 100644 --- a/src/app/preferences.rs +++ b/src/app/preferences.rs @@ -362,6 +362,15 @@ fn optional_text(value: &str) -> Option { (!value.is_empty()).then(|| value.to_owned()) } +fn update_runtime_preferences( + runtime_preferences: &RwLock, + preferences: &AppPreferences, +) { + *runtime_preferences + .write() + .unwrap_or_else(|poisoned| poisoned.into_inner()) = preferences.clone(); +} + impl App { pub(super) fn open_preferences(&mut self) { if self.database.is_none() || self.pending_project_path.is_some() || self.choosing_folder { @@ -457,9 +466,7 @@ impl App { Ok(preferences) => { self.preferences = preferences; #[cfg(target_os = "macos")] - if let Ok(mut runtime_preferences) = self.runtime_preferences.write() { - *runtime_preferences = self.preferences.clone(); - } + update_runtime_preferences(&self.runtime_preferences, &self.preferences); #[cfg(target_os = "macos")] if let Some(endpoint) = pending_endpoint { self._endpoint = Some(endpoint); @@ -474,3 +481,33 @@ impl App { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn runtime_preferences_update_after_lock_poisoning() { + let runtime = Arc::new(RwLock::new(AppPreferences::default())); + let poisoned = Arc::clone(&runtime); + let _ = std::thread::spawn(move || { + let _guard = poisoned.write().unwrap(); + panic!("poison preference lock"); + }) + .join(); + let preferences = AppPreferences { + endpoint_port: 4567, + ..AppPreferences::default() + }; + + update_runtime_preferences(&runtime, &preferences); + + assert_eq!( + runtime + .read() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .endpoint_port, + 4567 + ); + } +} diff --git a/src/server/tools.rs b/src/server/tools.rs index c648e2f..4a556cd 100644 --- a/src/server/tools.rs +++ b/src/server/tools.rs @@ -410,16 +410,17 @@ pub(super) fn validate_tool_results( if !matches!(protocol, Protocol::Anthropic | Protocol::Responses) { return Ok(()); } - let memory = state.tool_memory.lock().ok(); + let memory = state + .tool_memory + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); for (index, message) in messages.iter().enumerate() { if !matches!(message.role.as_str(), "tool" | "function") || message.tool_call_id.is_empty() { continue; } let id = &message.tool_call_id; - let live = memory - .as_ref() - .is_some_and(|memory| memory.contains_key(id)); + let live = memory.contains_key(id); let replayed = messages[..index].iter().any(|message| { message.role == "assistant" && message.tool_calls.iter().any(|call| call.id == *id) }); @@ -441,13 +442,15 @@ pub(super) fn validate_tool_results( } fn replayed_or_canonical_tools(state: &State, calls: &[ApiToolCall]) -> String { - if let Ok(memory) = state.tool_memory.lock() - && let Some(raw) = calls.iter().find_map(|call| { - (!call.id.is_empty()) - .then(|| memory.get(&call.id)) - .flatten() - }) - { + let memory = state + .tool_memory + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if let Some(raw) = calls.iter().find_map(|call| { + (!call.id.is_empty()) + .then(|| memory.get(&call.id)) + .flatten() + }) { return raw.clone(); } canonical_tools(calls) @@ -568,14 +571,16 @@ pub(super) fn parse_generated_tools_with_ids( .cloned() .unwrap_or_else(|| random_tool_id(prefix)); } - if let Ok(mut memory) = state.tool_memory.lock() { - // ponytail: one process-local replay table; add LRU eviction if 100k live tool ids is measured insufficient. - if memory.len() >= 100_000 { - memory.clear(); - } - for call in &calls { - memory.insert(call.id.clone(), raw.to_owned()); - } + let mut memory = state + .tool_memory + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + // ponytail: one process-local replay table; add LRU eviction if 100k live tool ids is measured insufficient. + if memory.len() >= 100_000 { + memory.clear(); + } + for call in &calls { + memory.insert(call.id.clone(), raw.to_owned()); } (content.to_owned(), calls) }