Summary
mempalace.backends.chroma.ChromaBackend caches PersistentClient instances in self._clients: dict[palace_path -> PersistentClient]. There's no public API to evict a client cleanly — the dict is the only handle downstream code has to "release this palace." Downstream code that pops from _clients (to bound cache growth or to tear down a palace) and then relies on Python GC to finalize the client hits SQLite file-lock issues on chromadb 1.5.x because chromadb requires an explicit client.close() before GC.
Repro
Minimum sequence within a single Python process:
from mempalace.backends.chroma import _DEFAULT_BACKEND
# 1. Open a palace (caches a PersistentClient under this path).
col = _DEFAULT_BACKEND.get_collection("/tmp/palace-a", create=True)
col.upsert(documents=["hello"], ids=["a"], metadatas=[{}])
# 2. "Evict" the palace — pop from _clients, let GC finish.
_DEFAULT_BACKEND._clients.pop("/tmp/palace-a", None)
# 3. Simulate a full teardown + re-create for the same path.
import shutil, os
shutil.rmtree("/tmp/palace-a")
os.makedirs("/tmp/palace-a")
# 4. Open again — fresh PersistentClient, fresh sqlite file.
col = _DEFAULT_BACKEND.get_collection("/tmp/palace-a", create=True)
# 5. First write fails.
col.upsert(documents=["world"], ids=["b"], metadatas=[{}])
# chromadb.errors.InternalError: Query error: Database error:
# error returned from database: (code: 1032) attempt to write a readonly database
SQLite error code 1032 is SQLITE_READONLY_DBMOVED — "the database file has been moved since it was opened." The rust-side chromadb bindings detect the inode change between the un-GC'd predecessor client and the fresh successor client and go into readonly mode.
Root cause
chromadb's Client.close() (introduced to release SQLite file locks explicitly; its docstring: "This is particularly important for PersistentClient to avoid SQLite file locking issues.") must be called on each cached client before it's removed from the dict. Dict-pop + Python GC is insufficient — chromadb retains rust-side state until the explicit close.
mempalace.backends.chroma.ChromaBackend currently offers no way to do this at the library boundary. Downstream integrators popping from _clients directly (the only handle available) end up holding the dict key change without the explicit close, which is the failure mode.
Proposed fix
Add a public close method to ChromaBackend that pops AND calls client.close():
# mempalace/backends/chroma.py (around existing _client method)
def close(self, palace_path: str) -> None:
"""Evict the cached PersistentClient for `palace_path` and release
its SQLite file locks. Required before `shutil.rmtree` on the palace
dir or before opening a fresh PersistentClient for the same path
within the same process lifetime.
"""
client = self._clients.pop(palace_path, None)
if client is not None:
try:
client.close()
except Exception:
# Forward-compat with older chromadb versions that didn't
# expose close(); GC fallback is what those versions used.
pass
Rename or supplement _clients.pop usage across the codebase to route through this method. Downstream integrators then call _DEFAULT_BACKEND.close(palace_path) and get the correct lifecycle.
Workaround downstream
Currently patching at our integration boundary:
# our PalaceHandle.close() equivalent
client = _DEFAULT_BACKEND._clients.pop(str(palace_path), None)
if client is not None:
try:
client.close()
except Exception:
pass
Works correctly, but every integrator will re-discover this (the symptom is invisible unless the integration explicitly tests close-then-reopen-to-same-path). Discovered during MemPalace-sidecar integration work (xelauvas/xelasphere, April 2026) when a GDPR-style DELETE + reopen test failed with SQLITE_READONLY_DBMOVED; happy to share the full benchmark/repro module.
Version
mempalace==3.3.1 (also checked v3.3.2 changelog — no fix mentioned).
chromadb==1.5.8.
Python 3.12.3, Linux.
Note on the related chromadb issue
Filed a separate issue upstream at chromadb (chroma-core/chroma#6941) about DefaultEmbeddingFunction.__call__ constructing a fresh ONNXMiniLM_L6_V2 per call (10× slowdown). That's an orthogonal concern — this issue is about ChromaBackend lifecycle, that one is about embedding-function caching.
Changelog observation
Scanning v3.0.0 → v3.3.2 release notes, I don't see a documented migration from "GC-sufficient close" to "explicit-close-required." If chromadb changed the contract between versions that MemPalace targets, a changelog/migration-guide entry would save the next integrator from replaying the diagnostic. Not blocking this issue; just flagging.
Summary
mempalace.backends.chroma.ChromaBackendcachesPersistentClientinstances inself._clients: dict[palace_path -> PersistentClient]. There's no public API to evict a client cleanly — the dict is the only handle downstream code has to "release this palace." Downstream code that pops from_clients(to bound cache growth or to tear down a palace) and then relies on Python GC to finalize the client hits SQLite file-lock issues on chromadb 1.5.x because chromadb requires an explicitclient.close()before GC.Repro
Minimum sequence within a single Python process:
SQLite error code 1032 is
SQLITE_READONLY_DBMOVED— "the database file has been moved since it was opened." The rust-side chromadb bindings detect the inode change between the un-GC'd predecessor client and the fresh successor client and go into readonly mode.Root cause
chromadb's
Client.close()(introduced to release SQLite file locks explicitly; its docstring: "This is particularly important for PersistentClient to avoid SQLite file locking issues.") must be called on each cached client before it's removed from the dict. Dict-pop + Python GC is insufficient — chromadb retains rust-side state until the explicit close.mempalace.backends.chroma.ChromaBackendcurrently offers no way to do this at the library boundary. Downstream integrators popping from_clientsdirectly (the only handle available) end up holding the dict key change without the explicit close, which is the failure mode.Proposed fix
Add a public close method to
ChromaBackendthat pops AND callsclient.close():Rename or supplement
_clients.popusage across the codebase to route through this method. Downstream integrators then call_DEFAULT_BACKEND.close(palace_path)and get the correct lifecycle.Workaround downstream
Currently patching at our integration boundary:
Works correctly, but every integrator will re-discover this (the symptom is invisible unless the integration explicitly tests close-then-reopen-to-same-path). Discovered during MemPalace-sidecar integration work (xelauvas/xelasphere, April 2026) when a GDPR-style DELETE + reopen test failed with SQLITE_READONLY_DBMOVED; happy to share the full benchmark/repro module.
Version
mempalace==3.3.1(also checked v3.3.2 changelog — no fix mentioned).chromadb==1.5.8.Python 3.12.3, Linux.
Note on the related chromadb issue
Filed a separate issue upstream at chromadb (chroma-core/chroma#6941) about
DefaultEmbeddingFunction.__call__constructing a freshONNXMiniLM_L6_V2per call (10× slowdown). That's an orthogonal concern — this issue is about ChromaBackend lifecycle, that one is about embedding-function caching.Changelog observation
Scanning v3.0.0 → v3.3.2 release notes, I don't see a documented migration from "GC-sufficient close" to "explicit-close-required." If chromadb changed the contract between versions that MemPalace targets, a changelog/migration-guide entry would save the next integrator from replaying the diagnostic. Not blocking this issue; just flagging.