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
- Retry process startup once — transient OOM/thread pressure clears
- Check thread and memory limits (ulimit -u, container pids/memory) and raise them if tight
- Run with a panic hook/backtrace and inspect logs immediately before the error
- 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
- Set generous container pids/memory limits
- Install a panic hook that logs before abort
- Retry startup once; persistent failures mean an environment or crate bug — report with the backtrace
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
- purge_agent not supported by this memory backend
- rename_agent not supported by this memory backend
- knowledge graph node limit reached ({}/{})
- SQLite connection open timed out after {} seconds
- cannot rename agent memory to `{to}`: an existing memory sto
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/7804bd90938d260a.
Report an issue: GitHub.