feat: optional remote ChromaDB support via HttpClient#294
feat: optional remote ChromaDB support via HttpClient#294cypromis wants to merge 29 commits intoMemPalace:mainfrom
Conversation
PR Review: feat: optional remote ChromaDB support via HttpClientExecutive Summary
Affected Areas: Business Impact: Enables multi-machine setups (e.g. mine on a laptop, serve via MCP on a headless server). Zero behavioral change for existing local users. Flow Changes: Every ChromaDB access now routes through Ratings
PR Health
High Priority Issues⚡ #1: PersistentClient created on every call — performance regression for MCP serverLocation:
- path = palace_path or cfg.palace_path
- os.makedirs(path, exist_ok=True)
- return chromadb.PersistentClient(path=path)
+ _persistent_clients = {} # cache: path -> PersistentClient
+
+ ...
+
+ path = palace_path or cfg.palace_path
+ if path not in _persistent_clients:
+ os.makedirs(path, exist_ok=True)
+ _persistent_clients[path] = chromadb.PersistentClient(path=path)
+ return _persistent_clients[path]🔗 #2: Incomplete migration —
|
|
Thanks for the review. Addressing each point: #1 — PersistentClient not cached (fixed): Confirmed regression. Added #2 — #3 — Dead globals in #4 — Subparser dispatch (fixed): Added a guard in #5 — #6 — Missing |
05c54d0 to
facb351
Compare
web3guru888
left a comment
There was a problem hiding this comment.
Really nice refactor — the palace_db factory is clean, well-tested, and the migration is thorough. A few observations:
1. Graph tool descriptions are stale after palace_graph.py migration
The MCP tool descriptions for mempalace_traverse, mempalace_find_tunnels, and mempalace_graph_stats were annotated with "reads from local palace only" (commit 8b92791), but palace_graph.py was subsequently migrated to use palace_db.get_collection() (commit 09d070b). Since the graph tools now go through the factory and respect remote mode, the "local only" caveat in the descriptions is inaccurate. Should be easy to remove.
2. get_or_create_collection called on every get_collection() — remote mode overhead
In remote mode, palace_db.get_collection() calls client.get_or_create_collection() which is an HTTP round-trip on every invocation. The client is cached but the collection object is not. For the MCP server processing many tool calls, this adds an HTTP call per dispatch. For v1 this is probably fine (the benchmark shows ~354µs locally), but for remote mode over network it could add up. A collection cache (similar to the client cache) would help.
3. uv.lock diff includes unrelated packages
The lock file changes include autocorrect and coverage — looks like a full uv lock sync that pulled in transitive deps unrelated to this PR. Not harmful but adds ~250 lines of noise. Consider regenerating the lockfile in a separate commit or PR to keep the diff focused.
4. ChromaDB version compatibility note
The PR correctly pins Docker to chromadb/chroma:0.6.3 to match the current chromadb>=0.5.0,<0.7 dependency. ChromaDB stable is now 1.5.5 and mixing 0.6.x client with 1.x server causes KeyError: '_type' — the README warning about this is good. Might be worth making the version constraint even more prominent since users' first instinct will be docker pull chromadb/chroma:latest.
Overall: solid work. The factory pattern is the right abstraction, the test coverage is good, and the README documentation is thorough. The issues above are minor and shouldn't block merge.
Add three new properties to MempalaceConfig supporting optional remote ChromaDB configuration: chroma_host (None = local mode), chroma_port (default 8000), and chroma_ssl (default False). All three support env var overrides (MEMPALACE_CHROMA_HOST/PORT/SSL) at higher priority than config file values. Empty string host env var is treated as absent.
Previously, a non-integer value in MEMPALACE_CHROMA_PORT was silently swallowed and fell back to the config file value, giving no indication that the env var was being ignored. Now raises ValueError with a clear message including the offending value. Adds a corresponding test to tests/test_palace_db.py to assert the error is raised and the message names the env var.
Introduces mempalace/palace_db.py as the central ChromaDB access point. get_client() returns HttpClient when chroma_host is configured, PersistentClient otherwise. get_collection() wraps get_or_create_collection for convenience. Exposes palace_db from the package and adds 4 factory tests (9 total in test_palace_db.py). Note: chromadb.PersistentClient is a factory function not a class, so tests assert on client.get_settings().is_persistent rather than isinstance checks.
Replace every direct chromadb.PersistentClient(path=...) call in production code with palace_db.get_client() / palace_db.get_collection(), so remote HttpClient support is automatically picked up wherever ChromaDB is accessed. Files changed: - mempalace/convo_miner.py: get_collection() → palace_db.get_collection() - mempalace/miner.py: get_collection() + status() → palace_db - mempalace/searcher.py: search() + search_memories() → palace_db.get_client() - mempalace/layers.py: all five layer classes → palace_db.get_client() - mempalace/mcp_server.py: _get_collection() → palace_db (palace_path threaded through so monkeypatched _config is respected in tests) - mempalace/cli.py: cmd_repair() + cmd_compress() → palace_db.get_client() Orphaned `import chromadb` lines removed from each migrated file. palace_graph.py and all test files are untouched.
Move `from . import palace_db` out of function bodies (_get_collection, cmd_repair, cmd_compress) and into the module-level import block of each file, following standard Python import hygiene.
Adds cmd_remote() handler and 'remote status' subcommand to the CLI. In local mode (no chroma_host), prints mode and palace path. In remote mode, calls client.heartbeat() to verify connectivity, exits 1 if unreachable. Adds test_remote_status_local_mode test to test_palace_db.py.
…e local-only
Replace all five palace_db.get_client().get_collection("mempalace_drawers")
chains in layers.py with palace_db.get_collection(), routing through the
DEFAULT_COLLECTION constant and supporting remote mode correctly.
Add a note to the Remote Mode section in README.md clarifying that graph
traversal MCP tools (mempalace_traverse, mempalace_find_tunnels,
mempalace_graph_stats) operate in local mode only.
…fix remaining string literals - palace_db.py: remove duplicate DEFAULT_COLLECTION string; import DEFAULT_COLLECTION_NAME from config.py as DEFAULT_COLLECTION so both names resolve to the same object - palace_db.py: add _http_clients dict cache keyed by (host, port, ssl) to avoid repeated HTTP connection overhead in long-lived processes (e.g. MCP server) - miner.py, searcher.py, cli.py: replace all hardcoded "mempalace_drawers" string literals at call sites with palace_db.DEFAULT_COLLECTION; only config.py retains the canonical definition - tests/test_palace_db.py: move mid-file imports (unittest.mock, chromadb, palace_db, subprocess, sys) to top of file; clear _http_clients cache in HttpClient test to ensure mock is always invoked
…er isolation as future PR
All ChromaDB access routes through two functions: - get_client(): returns HttpClient when chroma_host is configured, PersistentClient otherwise. Both are cached at module level so long-lived processes (MCP server) pay connection cost only once. - get_collection(): wraps get_or_create_collection via get_client(). - clear_caches(): explicit cache invalidation for test teardown and config reloads in long-running processes. palace_path is ignored in remote mode — the server manages storage.
…e_db palace_graph._get_collection() was calling chromadb.PersistentClient() directly, bypassing the new factory. In remote mode this caused a split-brain: graph MCP tools always read local data even when the rest of the system was configured to use a remote ChromaDB server. Replaced direct instantiation with palace_db.get_collection(), which honours the active config (local or remote) transparently.
- Remove _client_cache and _collection_cache module-level variables and the matching 'global' statement in _get_collection(). These were never populated after the palace_db refactor — actual caching now lives in palace_db._persistent_clients / _http_clients. - Fix hashlib.md5() call to pass usedforsecurity=False so the code does not raise ValueError on FIPS-enabled hosts (drawer ID generation only, not a security-sensitive use). - Annotate graph tool descriptions to note they read from local palace only — remote data is not included if remote mode is configured.
cmd_remote() had no guard for unrecognised subcommands — an unknown argument would silently fall through. Added an explicit check that prints to stderr and returns early, so callers (scripts, CI) get a non-zero exit code and a clear error message.
The existing autouse fixture reset mcp_server._client_cache and _collection_cache — attributes that no longer exist after the palace_db refactor. As a result, the real caches (palace_db._persistent_clients and _http_clients) were never cleared between tests, allowing client state to leak across test cases. Replaced with _reset_palace_db_caches which calls palace_db.clear_caches() before and after each test, restoring proper isolation.
…tests - test_clear_caches_empties_both_caches: verifies clear_caches() drains both _persistent_clients and _http_clients caches. - test_remote_status_unknown_subcommand_exits_nonzero: ensures 'mempalace remote badcmd' exits non-zero and prints the unknown command to stderr. - test_palace_graph_routes_through_palace_db: asserts that palace_graph._get_collection() calls palace_db.get_collection() and does not bypass the factory with a direct chromadb instantiation.
The bump-plugin-version workflow was triggered by push to main with no guard, meaning it could re-trigger itself if run with a PAT (which re-fires push events). Added a job-level condition: if: "!startsWith(github.event.head_commit.message, 'chore: bump version')" so the workflow skips commits it generated itself, preventing an infinite trigger loop.
ChromaDB 0.6.x ships with no authentication. The Docker compose snippet in the Remote Mode section binds port 8000 to all interfaces by default, giving anyone who can reach that port unrestricted read/write access to the user's memories. Added an explicit callout warning users to: - restrict binding to localhost or a trusted interface - use a firewall or VPN for multi-machine setups - never expose port 8000 to the public internet
Measures the five latency hotspots identified in the code review: 1. MempalaceConfig() instantiation cost (4–20 µs per get_client() call) 2. get_client() cold vs warm — 98× speedup on cache hit (446 µs → 5 µs) 3. get_collection() overhead — 354 µs per MCP dispatch 4. 4× independent col.get() vs 1 shared fetch — 4× speedup (2.9 ms → 728 µs) 5. MCP server hotpath simulation Run with: python benchmarks/factory_bench.py [--palace /path]
TODO.md: tracks two follow-up items deferred from this PR — caching MempalaceConfig() in palace_db.get_client() and per-user collection namespacing for remote multi-user setups. CLAUDE.md: remove palace_graph.py from the 'do not touch' constraint now that it has been migrated to use the palace_db factory.
f829d76 to
72a384f
Compare
Summary
v1 scope and known limitations
This PR targets one developer across multiple workstations. All clients share a single `mempalace_drawers` collection with no per-user namespacing or authentication.
ID collision risks when sharing between multiple users:
For single-dev multi-workstation use, collisions are harmless (re-mining the same file is idempotent). Per-user isolation — collection namespacing and optional authentication — is out of scope here and planned for a follow-up PR.
ChromaDB version compatibility
The Docker Compose snippet is pinned to `chromadb/chroma:0.6.3` to match this project's client dependency. ChromaDB 1.0 introduced a breaking change to the collection configuration JSON format (a `_type` field the 0.6.x client does not understand), causing `KeyError: '_type'` when creating or opening collections. Do not use `chromadb/chroma:latest` until MemPalace upgrades its client dependency.
Test plan