fix(mcp): avoid Chroma open when cached DB disappears

This commit is contained in:
Igor Lins e Silva 2026-06-14 12:12:59 -03:00
parent 6c5e1be3db
commit 97ba05cfe3
2 changed files with 52 additions and 4 deletions

View File

@ -164,10 +164,10 @@ def _get_result_ids(result) -> list:
if ids is not None:
return ids
if isinstance(result, dict):
return result.get("ids", [])
return result.get("ids") or []
getter = getattr(result, "get", None)
if callable(getter):
return getter("ids", [])
return getter("ids") or []
return []
@ -361,6 +361,7 @@ def _force_chroma_cache_reset() -> None:
_palace_db_mtime, \
_metadata_cache, \
_metadata_cache_time
cached_client = _client_cache
_client_cache = None
_collection_cache = None
_collection_cache_backend = None
@ -376,7 +377,24 @@ def _force_chroma_cache_reset() -> None:
backend = get_backend_for_palace(_config.palace_path)
backend.close_palace(PalaceRef(id=_config.palace_path, local_path=_config.palace_path))
except Exception:
pass
logger.debug("Failed to close cached Chroma backend during cache reset", exc_info=True)
if cached_client is not None:
try:
close = getattr(cached_client, "close", None)
if callable(close):
close()
except Exception:
logger.debug(
"Failed to close MCP-local Chroma client during cache reset", exc_info=True
)
try:
from chromadb.api.client import SharedSystemClient
clear_system_cache = getattr(SharedSystemClient, "clear_system_cache", None)
if callable(clear_system_cache):
clear_system_cache()
except Exception:
logger.debug("Failed to clear Chroma shared system cache during cache reset", exc_info=True)
# ── Vector-search disabled flag (#1222) ──────────────────────────────────
@ -687,6 +705,16 @@ def _get_collection(create=False):
}
return None
db_path = os.path.join(_config.palace_path, "chroma.sqlite3")
if not create and not os.path.isfile(db_path):
_force_chroma_cache_reset()
_collection_open_error = {
"error": "Chroma database missing",
"details": f"Could not open missing database at {db_path}.",
"hint": "Run: mempalace status or mempalace repair-status for diagnostics.",
}
return None
for attempt in range(2):
try:
if _collection_cache is not None and (

View File

@ -1239,6 +1239,16 @@ class TestWriteTools:
assert result["reason"] == "already_exists"
mock_col.upsert.assert_not_called()
def test_get_result_ids_normalizes_none_to_empty_list(self):
from mempalace import mcp_server
class DictLikeResult:
def get(self, key, default=None):
return None
assert mcp_server._get_result_ids({"ids": None}) == []
assert mcp_server._get_result_ids(DictLikeResult()) == []
def test_add_drawer_fails_when_readback_misses(self, monkeypatch, config, kg):
_patch_mcp_server(monkeypatch, config, kg)
from mempalace import mcp_server
@ -2385,10 +2395,20 @@ class TestCacheInvalidation:
if os.path.isfile(db_file):
os.remove(db_file)
make_client_calls = []
def fail_if_make_client_called(path):
make_client_calls.append(path)
raise AssertionError("_get_collection(create=False) should not open missing Chroma DB")
monkeypatch.setattr(mcp_server.ChromaBackend, "make_client", fail_if_make_client_called)
# Cache should be invalidated; _get_collection returns None
# because the backend can't open a missing DB without create=True
mcp_server._get_collection()
assert mcp_server._get_collection() is None
# The key assertion: the old cached collection was dropped
assert make_client_calls == []
assert mcp_server._collection_cache is None
assert mcp_server._palace_db_inode == 0
assert mcp_server._palace_db_mtime == 0.0