fix(core): degrade gracefully when sqlite-vec cannot load on init (#774)

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
Paul Hernandez
2026-04-29 08:57:31 -05:00
committed by GitHub
parent 3d927b848f
commit 2bfb9c76df
2 changed files with 107 additions and 2 deletions
@@ -94,9 +94,23 @@ class SQLiteSearchRepository(SearchRepositoryBase):
raise e
# Fail fast: create vector tables at startup so missing sqlite-vec
# or embedding provider errors surface immediately
# or embedding provider errors surface immediately.
# Trigger: the runtime semantic stack (sqlite-vec extension or embedding
# provider) is unavailable at startup.
# Why: failing the whole MCP boot for a search-only feature blocks
# Claude Desktop's handshake (#711). Keyword-only search is a
# reasonable fallback while the user resolves the dependency.
# Outcome: log the cause, mark this repository as semantic-disabled so
# downstream calls short-circuit cleanly, and let init complete.
if self._semantic_enabled:
await self._ensure_vector_tables()
try:
await self._ensure_vector_tables()
except SemanticDependenciesMissingError as exc:
logger.warning(
f"Semantic search disabled: {exc}. "
"Falling back to keyword-only search."
)
self._semantic_enabled = False
# ------------------------------------------------------------------
# FTS5 query preparation (backend-specific)
@@ -374,6 +388,25 @@ class SQLiteSearchRepository(SearchRepositoryBase):
async_connection = await session.connection()
raw_connection = await async_connection.get_raw_connection()
driver_connection = raw_connection.driver_connection
# Trigger: the underlying CPython was built without sqlite extension support.
# Why: python.org's macOS installer ships a stripped sqlite3 module with no
# enable_load_extension; when uvx happens to pick that interpreter (#711),
# the AttributeError surfaces here and previously crashed startup before
# Claude Desktop could complete its MCP handshake.
# Outcome: convert to SemanticDependenciesMissingError so the init-time
# handler can degrade gracefully to keyword search instead of dying.
if not hasattr(driver_connection, "enable_load_extension"):
raise SemanticDependenciesMissingError(
"This Python build does not support SQLite extension loading "
"(no enable_load_extension on sqlite3.Connection). "
"Common cause: python.org Python on macOS. "
"Reinstall basic-memory under a Python that ships extension "
"support (uv-managed CPython, Homebrew Python, or the official "
"Docker image), or set semantic_search_enabled=false in config "
"to silence this and use keyword-only search."
)
await driver_connection.enable_load_extension(True)
await driver_connection.load_extension(sqlite_vec.loadable_path())
await driver_connection.enable_load_extension(False)
@@ -107,6 +107,78 @@ async def test_init_search_index(search_repository, app_config):
assert table_name == "search_index"
@pytest.mark.asyncio
async def test_init_search_index_degrades_when_extension_loading_unavailable(
search_repository, monkeypatch
):
"""Regression for #711: when sqlite-vec cannot be loaded (e.g. python.org Python
3.12 ships sqlite3 without enable_load_extension), init must NOT crash. It should
log a warning, mark the repository as semantic-disabled, and let the rest of the
process come up so Claude Desktop's MCP handshake completes."""
if is_postgres_backend(search_repository):
pytest.skip("python.org enable_load_extension issue is SQLite-specific")
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
# Force the codepath even if semantic_search wasn't enabled by default.
search_repository._semantic_enabled = True
async def _raise_missing():
raise SemanticDependenciesMissingError("simulated: enable_load_extension missing")
monkeypatch.setattr(search_repository, "_ensure_vector_tables", _raise_missing)
# Must not raise — startup needs to complete even when the semantic stack is dead.
await search_repository.init_search_index()
assert search_repository._semantic_enabled is False, (
"Repository should mark itself semantic-disabled after a missing-deps error "
"so downstream calls short-circuit cleanly instead of re-attempting load."
)
@pytest.mark.asyncio
async def test_ensure_sqlite_vec_loaded_raises_typed_error_without_extension_support(
search_repository, monkeypatch
):
"""Regression for #711: AttributeError from a sqlite3.Connection that lacks
enable_load_extension must surface as SemanticDependenciesMissingError so the
init-time handler can degrade. Otherwise the AttributeError bubbles through and
crashes startup before Claude Desktop completes its handshake."""
if is_postgres_backend(search_repository):
pytest.skip("enable_load_extension is SQLite-specific")
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
from sqlalchemy.exc import OperationalError as SAOperationalError
# Stub session that always reports vec missing on probe, then yields a connection
# whose driver_connection has no enable_load_extension attribute (mirroring the
# python.org sqlite3 build).
class _StubDriverConnection:
# Deliberately omit enable_load_extension to mimic the python.org build.
pass
class _StubRawConnection:
driver_connection = _StubDriverConnection()
class _StubAsyncConnection:
async def get_raw_connection(self):
return _StubRawConnection()
class _StubSession:
async def execute(self, _stmt):
# First (and any) probe call reports vec missing.
raise SAOperationalError("SELECT vec_version()", {}, Exception("no vec"))
async def connection(self):
return _StubAsyncConnection()
with pytest.raises(SemanticDependenciesMissingError) as exc_info:
await search_repository._ensure_sqlite_vec_loaded(_StubSession())
assert "enable_load_extension" in str(exc_info.value)
@pytest.mark.asyncio
async def test_init_search_index_preserves_data(search_repository, search_entity):
"""Regression test: calling init_search_index() twice should preserve indexed data.