Hmbown/CodeWhale · error
active catalog write lock
Error message
active catalog write lock
What it means
`ActiveCatalogGuard::drop` calls `.write().expect("active catalog write lock")` on the global active-catalog RwLock. The panic fires only if the lock is poisoned, i.e. another thread panicked while holding the catalog lock and left it in an inconsistent state. This is a deliberate fail-fast: once the shared catalog invariant is broken the test harness must not silently continue with a corrupt catalog.
Solutions
- Fix the original panic that poisoned the lock — the poisoned-lock expect is a secondary failure
- Run tests single-threaded (`cargo test -- --test-threads=1`) to confirm which test poisons the catalog lock
- Scope catalog replacement so a panic while the lock is held releases it cleanly (avoid panicking between lock acquisition and guard drop)
- If recovery is desired, replace `.expect()` with `unwrap_or_else(PoisonError::into_inner)` to take the lock despite poisoning
Example fix
// before
let mut active = active_catalog().write().expect("active catalog write lock");
// after
let mut active = active_catalog().write().unwrap_or_else(PoisonError::into_inner); Defensive patterns
Strategy: try-catch
Validate before calling
// Best-effort pre-check in tests: detect poisoning before swapping
fn catalog_lock_healthy() -> bool {
std::panic::catch_unwind(|| active_catalog().read().is_ok()).unwrap_or(false)
} Try / catch
let mut active = active_catalog().write()
.unwrap_or_else(PoisonError::into_inner); // recover instead of panic
// or, to surface the original panic:
// match active_catalog().write() { Ok(g) => g, Err(poisoned) => panic!("catalog lock poisoned: {poisoned}") } Prevention
- Never panic while holding the active-catalog lock; clone data out, release, then assert
- Scope `replace_active_catalog_for_test` guards tightly so unwinds release them predictably
- Run catalog-touching tests with a serial group to avoid cross-test poisoning
When it happens
Trigger: Dropping an `ActiveCatalogGuard` returned by `replace_active_catalog_for_test` while the active-catalog RwLock is poisoned — typically because another test (or the guard itself) panicked while holding the lock earlier in the same process.
Common situations: Parallel test threads sharing the global catalog; a test that panics inside a closure holding the catalog read/write guard; a `replace_active_catalog_for_test` guard leaked across a panic boundary so the poisoned lock is re-locked on unwind.
Related errors
- A pinned task provider requires an explicit model
- Absolute path should not warn
- Agent continuation target is no longer retained
- Agent continuation target is outside the active session
- Agent not found
AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15).
Data as JSON: /api/errors/41fd3f1cc3accc23.
Report an issue: GitHub.
Appendix: source
Thrown at crates/models/src/model_catalog.rs:217
#[cfg(any(test, feature = "test-support"))]
static TEST_CATALOG_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
#[cfg(any(test, feature = "test-support"))]
pub fn test_catalog_lock() -> std::sync::MutexGuard<'static, ()> {
TEST_CATALOG_LOCK.lock().expect("model catalog test lock")
}
#[cfg(any(test, feature = "test-support"))]
pub struct ActiveCatalogGuard {
previous: MergedCatalog,
}
#[cfg(any(test, feature = "test-support"))]
impl Drop for ActiveCatalogGuard {
fn drop(&mut self) {
let mut active = active_catalog().write().expect("active catalog write lock");
*active = self.previous.clone();
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn replace_active_catalog_for_test(catalog: MergedCatalog) -> ActiveCatalogGuard {
let mut active = active_catalog().write().expect("active catalog write lock");
let previous = active.clone();
*active = catalog;
ActiveCatalogGuard { previous }
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(id: &str, context_window: u32, provenance: MetadataProvenance) -> CatalogEntry {
CatalogEntry {View on GitHub (pinned to 433685b202)