fix(backend): respect backend.persist_directory from palace mempalace.yaml - #1658
fix(backend): respect backend.persist_directory from palace mempalace.yaml#1658w1tc4 wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces dynamic resolution of the ChromaDB persistence directory by reading the backend.persist_directory configuration from mempalace.yaml. It updates several methods in ChromaBackend and mcp_server.py to use this resolved path. The feedback highlights that _resolve_persist_dir unconditionally creates directories, which introduces side effects during read-only operations like detect() or status checks. It is recommended to add a create flag to control directory creation and to add defensive type checks during YAML parsing.
| def _resolve_persist_dir(palace_path: str) -> str: | ||
| """Return the directory ChromaDB should use for this palace. | ||
|
|
||
| Reads ``<palace_path>/mempalace.yaml`` for ``backend.persist_directory``. | ||
| Relative paths are resolved against ``palace_path``; the directory is | ||
| created if it does not yet exist. Falls back to ``palace_path`` for | ||
| palaces that predate this config key (fully backwards-compatible). | ||
| """ | ||
| try: | ||
| import yaml # PyYAML — always available as a mempalace dependency | ||
|
|
||
| yaml_path = os.path.join(palace_path, "mempalace.yaml") | ||
| if os.path.isfile(yaml_path): | ||
| with open(yaml_path, encoding="utf-8") as fh: | ||
| cfg = yaml.safe_load(fh) or {} | ||
| persist = (cfg.get("backend") or {}).get("persist_directory") | ||
| if persist: | ||
| resolved = ( | ||
| persist | ||
| if os.path.isabs(persist) | ||
| else os.path.normpath(os.path.join(palace_path, persist)) | ||
| ) | ||
| os.makedirs(resolved, exist_ok=True) | ||
| return resolved | ||
| except Exception: | ||
| pass | ||
| return palace_path |
There was a problem hiding this comment.
The _resolve_persist_dir method currently calls os.makedirs(resolved, exist_ok=True) unconditionally. Since this method is called by read-only operations like detect(), _db_stat(), and status checks in mcp_server.py, it introduces an undesirable side effect of creating directories on the filesystem during simple detection or status probes. This can also lead to unexpected PermissionError exceptions if the user has read-only access to the parent directory.
Additionally, the YAML parsing is not fully defensive. If mempalace.yaml contains a non-dictionary top-level element or if backend is not a dictionary, it can raise an AttributeError (which is caught by the broad except Exception block but is better avoided with explicit type checks).
To fix this, we should add a create: bool = False parameter to _resolve_persist_dir and only call os.makedirs when create=True is explicitly requested (e.g., when initializing the PersistentClient). We should also add defensive type checks for the parsed YAML configuration.
@staticmethod
def _resolve_persist_dir(palace_path: str, create: bool = False) -> str:
"""Return the directory ChromaDB should use for this palace.
Reads "<palace_path>/mempalace.yaml" for "backend.persist_directory".
Relative paths are resolved against "palace_path"; the directory is
created if "create" is True and it does not yet exist. Falls back to
"palace_path" for palaces that predate this config key (fully
backwards-compatible).
"""
try:
import yaml # PyYAML — always available as a mempalace dependency
yaml_path = os.path.join(palace_path, "mempalace.yaml")
if os.path.isfile(yaml_path):
with open(yaml_path, encoding="utf-8") as fh:
cfg = yaml.safe_load(fh)
if isinstance(cfg, dict):
backend_cfg = cfg.get("backend")
if isinstance(backend_cfg, dict):
persist = backend_cfg.get("persist_directory")
if isinstance(persist, str) and persist.strip():
resolved = (
persist
if os.path.isabs(persist)
else os.path.normpath(os.path.join(palace_path, persist))
)
if create:
os.makedirs(resolved, exist_ok=True)
return resolved
except Exception:
pass
return palace_path| ChromaBackend._quarantined_paths.discard(palace_path) | ||
| ChromaBackend._prepare_palace_for_open(palace_path) | ||
| cached = chromadb.PersistentClient(path=palace_path) | ||
| cached = chromadb.PersistentClient(path=persist_dir) |
There was a problem hiding this comment.
Pass create=True to _resolve_persist_dir when initializing the PersistentClient to ensure the directory is created when the client is actually instantiated.
| cached = chromadb.PersistentClient(path=persist_dir) | |
| cached = chromadb.PersistentClient(path=ChromaBackend._resolve_persist_dir(palace_path, create=True)) |
| """ | ||
| ChromaBackend._prepare_palace_for_open(palace_path) | ||
| return chromadb.PersistentClient(path=palace_path) | ||
| return chromadb.PersistentClient(path=ChromaBackend._resolve_persist_dir(palace_path)) |
There was a problem hiding this comment.
Pass create=True to _resolve_persist_dir when initializing the PersistentClient to ensure the directory is created when the client is actually instantiated.
| return chromadb.PersistentClient(path=ChromaBackend._resolve_persist_dir(palace_path)) | |
| return chromadb.PersistentClient(path=ChromaBackend._resolve_persist_dir(palace_path, create=True)) |
|
The
|
aa41658 to
73b20f9
Compare
….yaml (MemPalace#1658) Wire _resolve_persist_dir() to every chroma.sqlite3 call site so that backend.persist_directory in mempalace.yaml is honoured by all code paths (writer, readers, search, repair, migrate, mcp_server, cli). Also preserve non-managed keys (backend, storage) when re-running init. Co-authored-by: witchsource <rachael.a.meehan@gmail.com>
4c723c4 to
3dcdd9a
Compare
|
Verifies that set persist_directory → mine → search all use the same subdir: DB lands in the configured location, not the palace root, and search_memories finds results without a 'no palace' error.
|
Thanks for this contribution, and apologies for the slow turnaround.
If you'd rather not pick it back up, no problem at all — just say so and I'll close it out, and thanks either way for taking the time to send it. |
Problem
backend.persist_directoryin a palace'smempalace.yamlwas silently ignored. ChromaDB was always initialized atpalace_pathroot because every call site hardcodedpath=palace_pathand no code ever read the palace-level YAML's backend section.Users who configured:
saw
chroma.sqlite3and the HNSW segment directories keep reappearing in the palace root on every restart — making OneDrive, Dropbox, and other sync tools surface internal DB files alongside user content, and making it impossible to keep the palace directory organized.Root cause
ChromaBackend._client(),make_client(),_db_stat(),_prepare_palace_for_open(), anddetect()all receivedpalace_pathand passed it straight tochromadb.PersistentClient(path=palace_path). There was no code path that read<palace_path>/mempalace.yamlat all.Fix
Add
ChromaBackend._resolve_persist_dir(palace_path)which:<palace_path>/mempalace.yamlforbackend.persist_directorypalace_pathpalace_pathfor unconfigured palaces (fully backwards-compatible)Wire it into every affected call site:
_db_stat— inode/mtime freshness check_client— db_path existence check +PersistentClientinit_prepare_palace_for_open— HNSW quarantine + blob seq_id fixmake_client— legacy entry pointdetect— backend auto-detectionmcp_server._get_client— stale-cache db_path probe (both occurrences)Testing