Skip to content

Commit 2bfb9c7

Browse files
authored
fix(core): degrade gracefully when sqlite-vec cannot load on init (#774)
Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 3d927b8 commit 2bfb9c7

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

src/basic_memory/repository/sqlite_search_repository.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,23 @@ async def init_search_index(self):
9494
raise e
9595

9696
# Fail fast: create vector tables at startup so missing sqlite-vec
97-
# or embedding provider errors surface immediately
97+
# or embedding provider errors surface immediately.
98+
# Trigger: the runtime semantic stack (sqlite-vec extension or embedding
99+
# provider) is unavailable at startup.
100+
# Why: failing the whole MCP boot for a search-only feature blocks
101+
# Claude Desktop's handshake (#711). Keyword-only search is a
102+
# reasonable fallback while the user resolves the dependency.
103+
# Outcome: log the cause, mark this repository as semantic-disabled so
104+
# downstream calls short-circuit cleanly, and let init complete.
98105
if self._semantic_enabled:
99-
await self._ensure_vector_tables()
106+
try:
107+
await self._ensure_vector_tables()
108+
except SemanticDependenciesMissingError as exc:
109+
logger.warning(
110+
f"Semantic search disabled: {exc}. "
111+
"Falling back to keyword-only search."
112+
)
113+
self._semantic_enabled = False
100114

101115
# ------------------------------------------------------------------
102116
# FTS5 query preparation (backend-specific)
@@ -374,6 +388,25 @@ async def _ensure_sqlite_vec_loaded(self, session) -> None:
374388
async_connection = await session.connection()
375389
raw_connection = await async_connection.get_raw_connection()
376390
driver_connection = raw_connection.driver_connection
391+
392+
# Trigger: the underlying CPython was built without sqlite extension support.
393+
# Why: python.org's macOS installer ships a stripped sqlite3 module with no
394+
# enable_load_extension; when uvx happens to pick that interpreter (#711),
395+
# the AttributeError surfaces here and previously crashed startup before
396+
# Claude Desktop could complete its MCP handshake.
397+
# Outcome: convert to SemanticDependenciesMissingError so the init-time
398+
# handler can degrade gracefully to keyword search instead of dying.
399+
if not hasattr(driver_connection, "enable_load_extension"):
400+
raise SemanticDependenciesMissingError(
401+
"This Python build does not support SQLite extension loading "
402+
"(no enable_load_extension on sqlite3.Connection). "
403+
"Common cause: python.org Python on macOS. "
404+
"Reinstall basic-memory under a Python that ships extension "
405+
"support (uv-managed CPython, Homebrew Python, or the official "
406+
"Docker image), or set semantic_search_enabled=false in config "
407+
"to silence this and use keyword-only search."
408+
)
409+
377410
await driver_connection.enable_load_extension(True)
378411
await driver_connection.load_extension(sqlite_vec.loadable_path())
379412
await driver_connection.enable_load_extension(False)

tests/repository/test_search_repository.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,78 @@ async def test_init_search_index(search_repository, app_config):
107107
assert table_name == "search_index"
108108

109109

110+
@pytest.mark.asyncio
111+
async def test_init_search_index_degrades_when_extension_loading_unavailable(
112+
search_repository, monkeypatch
113+
):
114+
"""Regression for #711: when sqlite-vec cannot be loaded (e.g. python.org Python
115+
3.12 ships sqlite3 without enable_load_extension), init must NOT crash. It should
116+
log a warning, mark the repository as semantic-disabled, and let the rest of the
117+
process come up so Claude Desktop's MCP handshake completes."""
118+
if is_postgres_backend(search_repository):
119+
pytest.skip("python.org enable_load_extension issue is SQLite-specific")
120+
121+
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
122+
123+
# Force the codepath even if semantic_search wasn't enabled by default.
124+
search_repository._semantic_enabled = True
125+
126+
async def _raise_missing():
127+
raise SemanticDependenciesMissingError("simulated: enable_load_extension missing")
128+
129+
monkeypatch.setattr(search_repository, "_ensure_vector_tables", _raise_missing)
130+
131+
# Must not raise — startup needs to complete even when the semantic stack is dead.
132+
await search_repository.init_search_index()
133+
134+
assert search_repository._semantic_enabled is False, (
135+
"Repository should mark itself semantic-disabled after a missing-deps error "
136+
"so downstream calls short-circuit cleanly instead of re-attempting load."
137+
)
138+
139+
140+
@pytest.mark.asyncio
141+
async def test_ensure_sqlite_vec_loaded_raises_typed_error_without_extension_support(
142+
search_repository, monkeypatch
143+
):
144+
"""Regression for #711: AttributeError from a sqlite3.Connection that lacks
145+
enable_load_extension must surface as SemanticDependenciesMissingError so the
146+
init-time handler can degrade. Otherwise the AttributeError bubbles through and
147+
crashes startup before Claude Desktop completes its handshake."""
148+
if is_postgres_backend(search_repository):
149+
pytest.skip("enable_load_extension is SQLite-specific")
150+
151+
from basic_memory.repository.semantic_errors import SemanticDependenciesMissingError
152+
from sqlalchemy.exc import OperationalError as SAOperationalError
153+
154+
# Stub session that always reports vec missing on probe, then yields a connection
155+
# whose driver_connection has no enable_load_extension attribute (mirroring the
156+
# python.org sqlite3 build).
157+
class _StubDriverConnection:
158+
# Deliberately omit enable_load_extension to mimic the python.org build.
159+
pass
160+
161+
class _StubRawConnection:
162+
driver_connection = _StubDriverConnection()
163+
164+
class _StubAsyncConnection:
165+
async def get_raw_connection(self):
166+
return _StubRawConnection()
167+
168+
class _StubSession:
169+
async def execute(self, _stmt):
170+
# First (and any) probe call reports vec missing.
171+
raise SAOperationalError("SELECT vec_version()", {}, Exception("no vec"))
172+
173+
async def connection(self):
174+
return _StubAsyncConnection()
175+
176+
with pytest.raises(SemanticDependenciesMissingError) as exc_info:
177+
await search_repository._ensure_sqlite_vec_loaded(_StubSession())
178+
179+
assert "enable_load_extension" in str(exc_info.value)
180+
181+
110182
@pytest.mark.asyncio
111183
async def test_init_search_index_preserves_data(search_repository, search_entity):
112184
"""Regression test: calling init_search_index() twice should preserve indexed data.

0 commit comments

Comments
 (0)