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

  1. Restart the app — a poisoned mutex cannot be un-poisoned in-process
  2. Find the original panic: search logs backwards for the panic message that precedes the first 'write lock poisoned' — fix that
  3. 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

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


AI-assisted analysis of tinyhumansai/openhuman@a221052e0d (2026-08-16). Data as JSON: /api/errors/814d64d32fbaf5ab. Report an issue: GitHub.