MemPalace/mempalace · error · BackendClosedError
ChromaBackend has been closed
Error message
ChromaBackend has been closed
What it means
A ValueError from _validate_candidate_strategy: search_memories was called with a candidate_strategy that is not a key of _CANDIDATE_MERGERS. Validation happens eagerly at the top of search_memories so the same error fires regardless of whether the call takes the vector path, BM25 fallback, or an early-error return — callers get consistent, immediate feedback instead of a downstream KeyError.
Source
Thrown at mempalace/backends/chroma.py:2285
Handles the palace-rebuild case (repair/nuke/purge) by invalidating the
cache when ``chroma.sqlite3`` changes on disk. Mirrors the semantics of
``mcp_server._get_client`` (merged via #757):
* DB file missing while we hold a cached client → drop the cache so we
do not serve stale data after a rebuild that has not yet re-created
the DB.
* Transition 0 → nonzero stat (DB created after cache) counts as a
change, so the cached client is replaced with one that sees the DB.
* FAT/exFAT filesystems return inode 0; we never fire inode comparisons
when either side is 0 (safe fallback) but still honor mtime.
* Mtime change uses an epsilon (0.01 s) to tolerate FS timestamp
granularity without thrashing.
"""
if self._closed:
from .base import BackendClosedError # late import avoids cycles at module load
raise BackendClosedError("ChromaBackend has been closed")
cached = self._clients.get(palace_path)
cached_inode, cached_mtime = self._freshness.get(palace_path, (0, 0.0))
current_inode, current_mtime = self._db_stat(palace_path)
db_path = os.path.join(palace_path, "chroma.sqlite3")
# DB was present when cache was built but is now missing → invalidate.
if cached is not None and not os.path.isfile(db_path):
_close_client(self._clients.pop(palace_path, None))
self._freshness.pop(palace_path, None)
cached = None
cached_inode, cached_mtime = 0, 0.0
inode_changed = current_inode != 0 and cached_inode != 0 and current_inode != cached_inode
# Transition from no-stat (0.0) to a real stat counts as a change so we
# pick up a DB that was created after the cache was built.
mtime_appeared = cached_mtime == 0.0 and current_mtime != 0.0
mtime_changed = (View on GitHub (pinned to 06cb6987f0)
Solutions
- Check the message: it lists the valid strategies as tuple(_CANDIDATE_MERGERS) — pick one of those.
- If the value comes from config, fix the config key (commonly a typo like 'union' vs 'merge').
- If you need a custom strategy, register a merger in _CANDIDATE_MERGERS rather than passing an unknown name.
Example fix
# before search_memories(q, candidate_strategy='merge-all') # after search_memories(q, candidate_strategy='union') # a registered name
Defensive patterns
Strategy: validation
Validate before calling
from mempalace.searcher import _CANDIDATE_MERGERS
if strategy not in _CANDIDATE_MERGERS:
raise SystemExit(f'bad strategy {strategy!r}; valid: {sorted(_CANDIDATE_MERGERS)}') Type guard
def is_valid_candidate_strategy(s: str) -> bool:
from mempalace.searcher import _CANDIDATE_MERGERS
return s in _CANDIDATE_MERGERS Prevention
- Validate config-supplied strategy names against available keys before calling search.
- Pin the mempalace version so strategy names in config match the installed merger registry.
When it happens
Trigger: Passing candidate_strategy='interleave' (or any typo/unimplemented name) to search_memories; strategy not among the registered merger names.
Common situations: Typos in config files (mempalace.yaml search settings), code written against an older/newer version whose strategy names differ, or dynamically-built strategy strings.
Related errors
- query requires exactly one of query_texts or query_embedding
- query requires query_embeddings
- query input must be a non-empty list
- milvus_consistency_level must be one of: {allowed}
- write routing scope must be 'hooks' or 'cli'
AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15).
Data as JSON: /api/errors/2d26c4d580f5d262.
Report an issue: GitHub.