zeroclaw-labs/zeroclaw · error

SQLite connection open timed out after {} seconds

Error message

SQLite connection open timed out after {} seconds

What it means

open_connection() optionally runs Connection::open on a helper thread and waits with recv_timeout, clamped to SQLITE_OPEN_TIMEOUT_CAP_SECS. If the timer expires (open blocking on a locked database file or a stalled filesystem), the thread is abandoned and this error reports the capped seconds — which may be lower than the configured value.

Source

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

    /// Open SQLite connection, optionally with a timeout (for locked/slow storage).
    fn open_connection(
        db_path: &Path,
        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,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Find and stop the other holder: lsof <workspace>/memory/brain.db (check the -wal/-shm files too)
  2. Raise the sqlite open timeout setting — but note it is clamped to SQLITE_OPEN_TIMEOUT_CAP_SECS, so values beyond the cap won't help
  3. Move the workspace (or at least brain.db) to fast local storage, not NFS/SMB
  4. If a dead process left stale locks, verify nothing is running, remove brain.db-wal and brain.db-shm, and retry
Defensive patterns

Strategy: retry

Validate before calling

// Before startup, detect another live holder of the database
// (shell): lsof <workspace>/memory/brain.db ; return non-zero output -> refuse to start
let locked = std::process::Command::new("lsof").arg(db_path).output()?.status.success();

Type guard

fn is_sqlite_open_timeout(e: &anyhow::Error) -> bool {
    e.to_string().starts_with("SQLite connection open timed out")
}

Try / catch

for attempt in 0..3 {
    match SqliteMemory::new(ws, timeout).await {
        Ok(m) => break Ok(m),
        Err(e) if is_sqlite_open_timeout(&e) && attempt < 2 => { free_locks_or_wait(); }
        Err(e) => break Err(e),
    }
}

Prevention

When it happens

Trigger: Another process holds workspace/memory/brain.db (a second gateway instance or stray daemon); the database lives on NFS/SMB where POSIX locks hang; very slow or failing storage; the configured timeout exceeds the internal cap.

Common situations: Two zeroclaw instances on one workspace; Docker bind-mounts on macOS/Windows with slow I/O; antivirus or indexers holding brain.db; NFS home directories.

Understand the failure class

Related errors


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