tinyhumansai/openhuman · error
whatsapp_data write lock poisoned: {e}
Error message
whatsapp_data write lock poisoned: {e} What it means
upsert_chats() acquires the store's write Mutex before entering write_with_corrupt_recovery; the lock is poisoned because another thread panicked while holding it, and .map_err converts the std::sync::PoisonError into this anyhow error. Every subsequent writer on this store instance fails identically until the process restarts — the poison is a symptom, the original panic is the disease.
Source
Thrown at app/src-tauri/src/whatsapp_data/store.rs:425
let result: String = conn
.query_row("PRAGMA integrity_check(1)", [], |row| row.get(0))
.context("running PRAGMA integrity_check")?;
Ok(result.eq_ignore_ascii_case("ok"))
}
/// Upsert chat metadata rows. Returns the number of rows inserted or updated.
pub fn upsert_chats(
&self,
account_id: &str,
chats: &HashMap<String, ChatMeta>,
) -> Result<usize> {
if chats.is_empty() {
return Ok(0);
}
let _write_guard = self
.write_lock
.lock()
.map_err(|e| anyhow::anyhow!("whatsapp_data write lock poisoned: {e}"))?;
self.write_with_corrupt_recovery("upsert_chats", || {
self.upsert_chats_inner(account_id, chats)
})
}
fn upsert_chats_inner(
&self,
account_id: &str,
chats: &HashMap<String, ChatMeta>,
) -> Result<usize> {
let conn = self.open_conn()?;
let now = Self::now_secs();
let mut count = 0usize;
for (chat_id, meta) in chats {
let name = meta.name.as_deref().unwrap_or("");
let is_group = chat_id.ends_with("@g.us") as i64;
conn.execute(
"INSERT INTO wa_chats (account_id, chat_id, display_name, is_group, updated_at)View on GitHub (pinned to a221052e0d)
Solutions
- Restart the app — a poisoned mutex cannot be un-poisoned in-process
- Find the original panic: search logs backwards for the panic message that precedes the first 'write lock poisoned' — fix that
- Harden the write path: no unwrap/expect inside the critical section; validate inputs before taking the lock
Example fix
// before — panicking under the lock poisons it for every later writer
let n = chats.len() as usize;
self.write_with_corrupt_recovery("upsert_chats", || self.upsert_chats_inner(account_id, chats))
// ...inside inner: let name = meta.name.expect("always set"); ← panics here
// after — return errors instead of panicking inside the critical section
let Some(name) = meta.name.clone() else {
anyhow::bail!("upsert_chats: chat {} missing name", chat_id);
}; Defensive patterns
Strategy: try-catch
Try / catch
match store.upsert_chats(account_id, &chats) {
Ok(n) => log::info!("[whatsapp_data] upserted {n} chats"),
Err(e) if e.to_string().contains("write lock poisoned") => {
log::error!("[whatsapp_data] store poisoned — clean restart required; batch re-derivable");
// do not retry in-process
}
Err(e) => return Err(e),
} Prevention
- never panic (unwrap/expect/index) while holding the write lock
- log the first panic loudly so later poison errors are traceable to it
- design ingestion to resume cleanly after an app restart
When it happens
Trigger: A panic inside any code holding write_lock (upsert_messages/prune/upsert_chats inner paths — e.g. an unwrap on a row invariant) poisons the mutex; the very next upsert_chats then fails here.
Common situations: A malformed chat map triggering an expect/unwrap in the write path; a rusqlite failure escalated to panic; code after error 294's corruption loop panicking mid-write.
Related errors
- AgentDefinitionRegistry missing after init
- learning_update_facet: {e:#}
- spawned core exited with ${child.exitCode} ${stderrFn()}
- timed out waiting for core at ${coreUrl} ${stderrFn()}
- rebuilt whatsapp_data db still fails integrity_check at {}
AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16).
Data as JSON: /api/errors/814d64d32fbaf5ab.
Report an issue: GitHub.