#2535·mempalace

mcp_server._get_client has no own-write re-stamp: every add_drawer and check_duplicate reloads the HNSW index (#2307 covered only ChromaBackend)

Author: christianlappinCreated Sep 16, 2026Updated Sep 16, 2026

What happens

#2307 stopped ChromaBackend._client invalidating its cache on the backend's own writes, by re-stamping the freshness stat once an operation finishes. mcp_server._get_client keys on the same chroma.sqlite3 mtime but never re-stamps, and _get_collection calls it on every tool call (v3.9.0 mempalace/mcp_server.py:1604). So after the server's own write, the next MCP tool that goes through _get_collection sees its own footprint as an external change, runs _force_chroma_cache_reset() and reopens, and the next vector query reloads the index from disk.

Search is unaffected: it goes through the backend path that #2307 fixed.

Measured

mempalace 3.9.0 (pip, clean venv), chromadb 1.5.9, Python 3.11, macOS 26 arm64, chroma backend, a scratch palace of 40 mined notes. One in-process server driven through handle_request, counting chromadb System creations per call (a new System means the HNSW index is read from disk again):

Call Reloads on 3.9.0
status 0
search (first) 1 (cold open, expected)
search again 0
check_duplicate 1
search 0
add_drawer 1
search, search 0
add_drawer 1
check_duplicate 1
list_drawers 0
search 0
(another process mines)
search 1 (correct: it picks up the new drawers)

6 reloads in 14 calls. Four of them are the server reacting to its own writes.

Cost per reload is palace-sized. On our 151k-drawer palace a reload takes about 2.5 s (2.9 s for a search that reopens, against 0.2 s warm). That timing is from our own 3.6.0 install; we did not repeat it against 3.9.0 on a palace that size.

Reproduction

python
# after: pip install mempalace==3.9.0, and a palace with some drawers in wing "seed"
import mempalace.mcp_server as m
from chromadb.api.shared_system_client import SharedSystemClient

created = []
orig = SharedSystemClient._create_system_if_not_exists.__func__
def counting(cls, identifier, settings):
    if identifier not in cls._identifier_to_system:
        created.append(identifier)
    return orig(cls, identifier, settings)
SharedSystemClient._create_system_if_not_exists = classmethod(counting)

def call(tool, args):
    before = len(created)
    m.handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/call",
                      "params": {"name": tool, "arguments": args}})
    print(f"{tool:<26} reloads={len(created) - before}")

q = {"query": "whatever is in your palace", "wing": "seed", "limit": 3}
call("mempalace_search", q)          # 1, cold open
call("mempalace_search", q)          # 0
call("mempalace_add_drawer", {"wing": "probe", "room": "own", "content": "x", "added_by": "p"})  # 1
call("mempalace_check_duplicate", {"content": "x"})  # 1

Suggested direction

The same shape as #2307, on the MCP path: after the server's own write completes, and after _get_collection has opened a collection, re-stat chroma.sqlite3 into _palace_db_inode / _palace_db_mtime, so a later difference is genuinely another process's write. The same trade-off #2307 documents applies: an external write landing inside one of the server's own operations is absorbed until the next change.

If you would rather not rely on mtime at all on this path, the signal we ended up using downstream is the write-ahead log high-water mark (SELECT max(seq_id) FROM embeddings_queue): reads never move it, and writes from any process always do. That needs another connection to chroma.sqlite3, so it interacts with #2302 / #2506; on a palace in WAL mode it should go through whatever _inproc_sqlite ends up being.

What this report does not claim

  • Searches on 3.9.0 are fine, and do see other processes' writes.
  • Not tested through the hub/proxy path (mempalace serve), only a stdio server.
  • The palace here is small; the reload count is the finding, not the timing.

Found while keeping a 3.6.0 install alive on a shared palace; the measurements above are from a clean 3.9.0 venv with no local patches. Investigation and write-up by Claude Opus 5 running in Claude Code on my machine.