feat: repair nuke-rebuild, purge command, --version flag#632
feat: repair nuke-rebuild, purge command, --version flag#632jphein wants to merge 4 commits intoMemPalace:developfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves MemPalace’s maintenance CLI by making index rebuilds more reliable under ChromaDB HNSW corruption scenarios, adding a targeted purge workflow, exposing a standard --version/-v flag, and updating the ChromaDB dependency range to a newer major line.
Changes:
- Update
mempalace repairto rebuild by extracting drawers, deleting the palace directory, recreating a freshPersistentClient/collection (cosine), and performing a post-rebuild verification query. - Add
mempalace purge --wing <w> [--room <r>] [-y]to remove drawers by metadata filter via extract-keep → nuke → reinsert into a fresh collection. - Add
--version/-vflag and bumpchromadbdependency to>=1.5.4,<2.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
pyproject.toml |
Bumps the ChromaDB dependency range to the 1.x line. |
mempalace/cli.py |
Adds purge, changes repair to directory-nuke rebuild + verification, and introduces --version/-v. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Nuke and rebuild with clean HNSW index | ||
| palace_path = palace_path.rstrip(os.sep) | ||
| print(" Rebuilding palace...") | ||
| shutil.rmtree(palace_path) | ||
| os.makedirs(palace_path, mode=0o700) | ||
|
|
There was a problem hiding this comment.
cmd_purge deletes the entire palace_path with shutil.rmtree() but never creates the backup directory described in the docstring. This makes the command irreversibly destructive if the rebuild/add step fails partway through; consider creating a backup (e.g., copy/move to palace_path + ".backup") before deleting, and printing the backup location for recovery.
There was a problem hiding this comment.
Good catch — purge should create a backup before nuking, like repair does. Will add.
| del col, client | ||
|
|
||
| # Nuke and rebuild with clean HNSW index | ||
| palace_path = palace_path.rstrip(os.sep) |
There was a problem hiding this comment.
palace_path = palace_path.rstrip(os.sep) can turn a root path into an unsafe target (e.g., '/' -> '' on POSIX, 'C:\\' -> 'C:' on Windows). Because this is followed by shutil.rmtree(palace_path), add a defensive validation step (resolve to an absolute path and reject filesystem roots / empty paths) before deleting anything.
| palace_path = palace_path.rstrip(os.sep) | |
| palace_path = palace_path.rstrip(os.sep) | |
| palace_path = os.path.abspath(palace_path) | |
| palace_root = str(Path(palace_path).anchor or os.path.abspath(os.sep)) | |
| if not palace_path or palace_path == palace_root: | |
| raise ValueError(f"Refusing to delete unsafe palace path: {palace_path!r}") |
There was a problem hiding this comment.
Good catch — will add a guard rejecting paths that resolve to filesystem root.
| except Exception: | ||
| print(f"\n No palace found at {palace_path}") |
There was a problem hiding this comment.
The except Exception: branch reports "No palace found" for any failure when opening ChromaDB / the collection (including permission errors or a corrupt DB). Include the exception message (or catch a narrower exception) so users can distinguish "missing" from "unreadable/corrupt" palaces.
| except Exception: | |
| print(f"\n No palace found at {palace_path}") | |
| except Exception as exc: | |
| print(f"\n Failed to open palace at {palace_path}: {exc}") |
There was a problem hiding this comment.
Will include the exception message in the error output.
| Not idempotent — running purge twice on the same criteria will fail the | ||
| second time if the first run completed (nothing left to match). The | ||
| backup directory is preserved for recovery. |
There was a problem hiding this comment.
The cmd_purge docstring says the operation is "Not idempotent" and "will fail" on a second run, but the implementation returns cleanly with a "No drawers found" message when nothing matches. Update the docstring to match the actual behavior (or adjust behavior to match the docstring).
| Not idempotent — running purge twice on the same criteria will fail the | |
| second time if the first run completed (nothing left to match). The | |
| backup directory is preserved for recovery. | |
| Safe to re-run with the same criteria: if no drawers match, the command | |
| exits cleanly after reporting that nothing was found. The backup | |
| directory is preserved for recovery. |
There was a problem hiding this comment.
Will update the docstring — a second run returns cleanly, it doesn't fail.
| palace_path = palace_path.rstrip(os.sep) | ||
| backup_path = palace_path + ".backup" | ||
| if os.path.exists(backup_path): | ||
| shutil.rmtree(backup_path) | ||
| print(f" Backing up to {backup_path}...") | ||
| shutil.copytree(palace_path, backup_path) | ||
|
|
||
| print(" Rebuilding collection...") | ||
| client.delete_collection("mempalace_drawers") | ||
| new_col = client.create_collection("mempalace_drawers") | ||
| # Nuke and recreate — fresh PersistentClient gets a clean HNSW index | ||
| print(" Rebuilding from scratch...") | ||
| shutil.rmtree(palace_path) | ||
| os.makedirs(palace_path, mode=0o700) | ||
|
|
There was a problem hiding this comment.
cmd_repair also uses palace_path.rstrip(os.sep) followed by shutil.rmtree(palace_path), which can become dangerous if the user passes a filesystem root (or Windows drive root). Add a guard that rejects empty/root paths after normalization before deleting the directory.
There was a problem hiding this comment.
Same fix will apply to both repair and purge.
| print(" Index verification: OK") | ||
| except Exception as e: | ||
| print(f" Index verification FAILED: {e}") | ||
| print(f" Restore from backup: mv {backup_path} {palace_path}") |
There was a problem hiding this comment.
On index verification failure, the suggested restore command uses mv, which won’t work on Windows and may not be available in all environments. Consider printing a cross-platform restore instruction (e.g., using Python/shutil terminology) or detect the OS when formatting the guidance.
| print(f" Restore from backup: mv {backup_path} {palace_path}") | |
| restore_cmd = ( | |
| "python -c " | |
| + shlex.quote( | |
| "from pathlib import Path; import shutil; " | |
| f"shutil.move(str(Path({str(backup_path)!r})), str(Path({str(palace_path)!r})))" | |
| ) | |
| ) | |
| print(f" Restore from backup: {restore_cmd}") |
There was a problem hiding this comment.
Fair point, though this is a CLI tool primarily used on Unix. Will note both mv and manual copy as restore options.
| ] | ||
| dependencies = [ | ||
| "chromadb>=0.5.0,<0.7", | ||
| "chromadb>=1.5.4,<2", |
There was a problem hiding this comment.
The dependency constraint is bumped to chromadb>=1.5.4,<2, but the repository’s committed lockfile (uv.lock) still pins ChromaDB 0.6.3. If contributors rely on uv.lock for reproducible environments, regenerate/update it so it matches this new constraint.
| "chromadb>=1.5.4,<2", | |
| "chromadb==0.6.3", |
There was a problem hiding this comment.
Will update uv.lock to match the new pin.
There was a problem hiding this comment.
Fixed — regenerated uv.lock with uv lock. chromadb 0.6.3 → 1.5.7. 332f8ac
| parser.add_argument( | ||
| "--version", "-v", action="version", version=f"mempalace {__version__}" | ||
| ) |
There was a problem hiding this comment.
The new --version / -v flag added here doesn’t appear to have a corresponding CLI test (e.g., asserting the printed version string and exit code), even though other main() behaviors are covered in tests/test_cli.py. Add a test that invokes main() with --version and asserts the output includes the package version.
There was a problem hiding this comment.
Will add a test for --version output.
There was a problem hiding this comment.
Fixed — added test_version_flag_prints_version that verifies __version__ appears in output and exit code is 0. 332f8ac
| p_purge = sub.add_parser("purge", help="Delete drawers by wing and/or room (rebuilds index)") | ||
| p_purge.add_argument("--wing", help="Wing to purge") | ||
| p_purge.add_argument("--room", help="Room to purge (without --wing, purges across ALL wings)") | ||
| p_purge.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt") | ||
|
|
There was a problem hiding this comment.
The new purge subcommand wiring added here isn’t covered by existing CLI tests (there are dispatch tests for other subcommands in tests/test_cli.py). Add tests for mempalace purge argument validation (no args → error) and dispatch/execution wiring.
There was a problem hiding this comment.
Will add purge CLI tests.
There was a problem hiding this comment.
Fixed — added 3 purge tests: test_cmd_purge_no_args_prints_error, test_cmd_purge_no_matching_drawers, test_cmd_purge_no_palace. 332f8ac
- repair: nukes palace directory and recreates PersistentClient instead of same-process delete_collection+create_collection, which reuses corrupted HNSW state. Adds index verification after rebuild. - purge: new command to delete drawers by wing/room with index rebuild. Extracts keepers, nukes palace, re-inserts into clean ChromaDB. - --version / -v flag shows mempalace version - chromadb pin bumped to >=1.5.4,<2 Co-Authored-By: Claude Opus 4.6 <[email protected]>
- Add backup before nuke in purge (was missing, unlike repair) - Guard against rstrip(os.sep) resolving to filesystem root - Include exception message when palace can't be opened - Fix docstring: second run returns cleanly, doesn't fail Co-Authored-By: Claude Opus 4.6 <[email protected]>
…tests - Regenerate uv.lock: chromadb 0.6.3 → 1.5.7 to match pyproject.toml pin - Add --version flag test: verifies version string in output - Add purge CLI tests: no args error, no matching drawers, no palace Co-Authored-By: Claude Opus 4.6 <[email protected]>
Upstream bumped pyproject.toml to 3.2.0 in MemPalace#761 but missed version.py. Aligns the two so test_version_consistency passes. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
Closing this as superseded — all three proposed features shipped in upstream v3.3.0:
Thanks to whoever did the parallel work in this area. Closing to keep the PR list focused on what's actually outstanding. |
MemPalace#629 and MemPalace#632 were closed upstream as fully superseded — all three features in MemPalace#632 (`--version`, `purge`, `repair`) shipped in v3.3.0; MemPalace#629's core (batching, file locking, skip patterns, stopwords) landed independently via upstream's own paths. Move both to the closed list and update the live/closed counts. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
Palace maintenance improvements. Split from #562 per maintainer request (PR 4 of 6).
mempalace repairnow nukes the entire palace directory and creates a fresh PersistentClient, instead of using same-processdelete_collection+create_collectionwhich reuses corrupted HNSW state (hnswlib updatePoint race on modified-file re-mine: EXC_BAD_ACCESS in repairConnectionsForUpdate (macOS ARM64, Python 3.13, chromadb 0.6.3) #521). Adds post-rebuild index verification query.mempalace purge --wing <w> [--room <r>]to delete drawers by wing/room with a clean index rebuild. Extracts keepers, nukes palace dir, re-inserts into fresh ChromaDB.--version/-vflag:mempalace --versionnow shows the version (SKILL.md uses nonexistentmempalace --version; init instructions omit--yesso agents hit EOFError #534)>=1.5.4,<2(was>=0.5.0,<0.7) — required for WAL rotation and HNSW stability (Readme chromadb version differs from actual #531)Both repair and purge release the ChromaDB client before
shutil.rmtree()to avoid file handle conflicts on Windows/some Linux FS, and usehnsw:space=cosinefor the new collection.Closes
mempalace --version; init instructions omit--yesso agents hit EOFError #534 — SKILL.md uses nonexistentmempalace --versionRelated
Test plan
mempalace --versionprints versionmempalace -vprints versionmempalace repairon a palace with data: backs up, nukes, rebuilds, verifies indexmempalace purge --wing conversations -y: removes matching drawers, rebuilds with remainingmempalace purge --room backend: purges room across all wingsmempalace purgewith no args: shows error