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

  1. Check the message: it lists the valid strategies as tuple(_CANDIDATE_MERGERS) — pick one of those.
  2. If the value comes from config, fix the config key (commonly a typo like 'union' vs 'merge').
  3. 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

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


AI-assisted analysis of MemPalace/mempalace@06cb6987f0 (2026-08-15). Data as JSON: /api/errors/2d26c4d580f5d262. Report an issue: GitHub.