diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index 71dfc90..6053a35 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -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 ( diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index a5d3088..2afdd86 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -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