zeroclaw-labs/zeroclaw · error

SQLite open thread exited unexpectedly

Error message

SQLite open thread exited unexpectedly

What it means

open_connection()'s helper thread always sends its Connection::open result over a channel (even errors); RecvTimeoutError::Disconnected means the channel closed without a send — practically the thread panicked or died. Near-unreachable in practice; treat it as an environment or bug signal, not a config error.

Source

Thrown at crates/zeroclaw-memory/src/sqlite.rs:156

        open_timeout_secs: Option<u64>,
    ) -> anyhow::Result<Connection> {
        let path_buf = db_path.to_path_buf();

        let conn = if let Some(secs) = open_timeout_secs {
            let capped = secs.min(SQLITE_OPEN_TIMEOUT_CAP_SECS);
            let (tx, rx) = mpsc::channel();
            thread::spawn(move || {
                let result = Connection::open(&path_buf);
                let _ = tx.send(result);
            });
            match rx.recv_timeout(Duration::from_secs(capped)) {
                Ok(Ok(c)) => c,
                Ok(Err(e)) => return Err(e).context("SQLite failed to open database"),
                Err(mpsc::RecvTimeoutError::Timeout) => {
                    anyhow::bail!("SQLite connection open timed out after {} seconds", capped);
                }
                Err(mpsc::RecvTimeoutError::Disconnected) => {
                    anyhow::bail!("SQLite open thread exited unexpectedly");
                }
            }
        } else {
            Connection::open(&path_buf).context("SQLite failed to open database")?
        };

        Ok(conn)
    }

    /// Initialize all tables: memories, FTS5, `embedding_cache`
    fn init_schema(conn: &Connection) -> anyhow::Result<()> {
        fn is_db_locked_error(e: &rusqlite::Error) -> bool {
            use rusqlite::ffi::ErrorCode;
            matches!(
                e,
                rusqlite::Error::SqliteFailure(err, _)
                    if matches!(err.code, ErrorCode::DatabaseBusy | ErrorCode::DatabaseLocked)
            )

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry process startup once — transient OOM/thread pressure clears
  2. Check thread and memory limits (ulimit -u, container pids/memory) and raise them if tight
  3. Run with a panic hook/backtrace and inspect logs immediately before the error
  4. If reproducible, update rusqlite/zeroclaw and file an issue with the backtrace
Defensive patterns

Strategy: try-catch

Validate before calling

// Before startup, confirm headroom so the open thread can't die
// shell: ulimit -u (threads) and container memory limit comfortably above current usage

Type guard

fn is_sqlite_open_thread_died(e: &anyhow::Error) -> bool {
    e.to_string().starts_with("SQLite open thread exited unexpectedly")
}

Try / catch

if let Err(e) = SqliteMemory::new(ws, timeout).await {
    if is_sqlite_open_thread_died(&e) { return Err(e.context("restart the process; if it repeats, capture a panic backtrace")); }
    return Err(e);
}

Prevention

When it happens

Trigger: A panic inside Connection::open on the helper thread (allocator/OOM failure, stack exhaustion in SQLite FFI), thread-limit exhaustion, or a rusqlite/zeroclaw defect in the open path.

Common situations: Containers at memory or pids limits; hosts under heavy memory pressure; rare rusqlite ABI issues after a partial upgrade.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/7804bd90938d260a. Report an issue: GitHub.