diff --git a/mempalace/backends/chroma.py b/mempalace/backends/chroma.py index 14ae9cd..c36e2c7 100644 --- a/mempalace/backends/chroma.py +++ b/mempalace/backends/chroma.py @@ -566,7 +566,9 @@ class ChromaBackend(BaseBackend): if create: collection = client.get_or_create_collection( - collection_name, metadata={"hnsw:space": hnsw_space}, **ef_kwargs + collection_name, + metadata={"hnsw:space": hnsw_space, "hnsw:num_threads": 1}, + **ef_kwargs, ) else: collection = client.get_collection(collection_name, **ef_kwargs) @@ -613,7 +615,9 @@ class ChromaBackend(BaseBackend): ef = self._resolve_embedding_function() ef_kwargs = {"embedding_function": ef} if ef is not None else {} collection = self._client(palace_path).create_collection( - collection_name, metadata={"hnsw:space": hnsw_space}, **ef_kwargs + collection_name, + metadata={"hnsw:space": hnsw_space, "hnsw:num_threads": 1}, + **ef_kwargs, ) return ChromaCollection(collection) diff --git a/mempalace/hooks_cli.py b/mempalace/hooks_cli.py index 01eca3f..bdcc97d 100644 --- a/mempalace/hooks_cli.py +++ b/mempalace/hooks_cli.py @@ -643,6 +643,9 @@ def hook_session_start(data: dict, harness: str): _output({}) +MAX_PRECOMPACT_BLOCK_ATTEMPTS = 2 + + def hook_precompact(data: dict, harness: str): """Precompact hook: mine transcript synchronously, then allow compaction.""" parsed = _parse_harness_input(data, harness) diff --git a/mempalace/miner.py b/mempalace/miner.py index b593797..2fde777 100644 --- a/mempalace/miner.py +++ b/mempalace/miner.py @@ -20,10 +20,12 @@ from typing import Optional from .palace import ( NORMALIZE_VERSION, SKIP_DIRS, + MineAlreadyRunning, build_closet_lines, file_already_mined, get_closets_collection, get_collection, + mine_global_lock, mine_lock, purge_file_closets, upsert_closet_lines, @@ -993,6 +995,48 @@ def mine( ``mine`` walks the tree itself just like before. """ + if dry_run: + return _mine_impl( + project_dir, + palace_path, + wing_override=wing_override, + agent=agent, + limit=limit, + dry_run=dry_run, + respect_gitignore=respect_gitignore, + include_ignored=include_ignored, + ) + + try: + with mine_global_lock(): + return _mine_impl( + project_dir, + palace_path, + wing_override=wing_override, + agent=agent, + limit=limit, + dry_run=dry_run, + respect_gitignore=respect_gitignore, + include_ignored=include_ignored, + ) + except MineAlreadyRunning: + print( + "mempalace: another `mine` is already running — exiting cleanly.", + file=sys.stderr, + ) + return + + +def _mine_impl( + project_dir: str, + palace_path: str, + wing_override: str = None, + agent: str = "mempalace", + limit: int = 0, + dry_run: bool = False, + respect_gitignore: bool = True, + include_ignored: list = None, +): project_path = Path(project_dir).expanduser().resolve() config = load_config(project_dir) diff --git a/mempalace/palace.py b/mempalace/palace.py index a2a4a8e..76a037e 100644 --- a/mempalace/palace.py +++ b/mempalace/palace.py @@ -310,6 +310,69 @@ def mine_lock(source_file: str): lf.close() +class MineAlreadyRunning(RuntimeError): + """Raised when another `mempalace mine` process already holds the global lock.""" + + +@contextlib.contextmanager +def mine_global_lock(): + """Process-wide non-blocking lock around the full `mine` pipeline. + + The per-file `mine_lock` only protects delete+insert interleave for a + single source; it does not prevent N copies of `mempalace mine