fix(cli): cleanup local DB state on set-cloud/set-local#775
Conversation
Per maintainer direction (#680), `set-cloud` is now a one-way cutover rather than a routing toggle. Before this change, `set-cloud` only flipped `mode = cloud` in config and left the project's row in the local index DB alone — so `_merge_projects` continued to see the project on both sides and reported `source: "local+cloud"`, with the old `local_path` still populated. Confusing state, no clean way out without manually editing both `config.json` and `memory.db`. Changes: - `set-cloud` now drops the project's row from the local index DB and clears `path` in config. On-disk note files are preserved — the user can keep, archive, or delete them as they like. - `set-local` is the symmetric inverse. It accepts `--local-path` and recreates the local DB row at the resolved path. If the prior path is still in config (e.g. set-local on a project that was already local), `--local-path` may be omitted and the existing path is reused. - Both helpers (`_detach_local_project_row`, `_attach_local_project_row`) are idempotent: running set-cloud twice doesn't error; running set-local twice updates the path if it changed and otherwise no-ops. Tests: - `test_set_cloud_clears_path` — config `path` blanked after set-cloud - `test_set_local_requires_path_after_set_cloud` — regression for the exact loop the reporter described: re-binding to local must not silently default - `test_set_local_success` — full round trip with an explicit path Closes #680 Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 37s —— View job Code Review:
|
| Item | Status |
|---|---|
| Follows CLAUDE.md coding conventions | |
| Python 3.12+ type annotations | app_config untyped |
| SQLAlchemy 2.0 async patterns | ✅ |
| FastAPI / Typer conventions | ✅ |
| 100-character line limit | ✅ |
| No commented-out code | ✅ |
| Unit tests for new functions | |
| 100% test coverage maintained | return True and update_path branches untested |
| Literate programming / Trigger-Why-Outcome comments | ✅ Excellent |
| DB changes / Alembic migrations | ✅ N/A (no schema changes) |
| Local-first architecture preserved | ✅ |
| Knowledge graph consistency | ✅ |
| On-disk file integrity | ✅ Explicitly not touched |
| No hardcoded secrets | ✅ |
| Input validation | ✅ |
| Error handling | ✅ Idempotent, fail-fast |
| Windows path compatibility | str(path) instead of Path.as_posix() |
| No emojis/unicode in console output | ✅ |
Summary: Solid fix for the confusing local+cloud merge state. The logic is correct and the docstrings are well-structured. The three actionable issues are: add BasicMemoryConfig type annotations to the two helpers, fix Windows-incompatible path assertions in two test cases, and add tests for the uncovered branches (return True in detach, update_path in attach).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a42c6a2a28
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) | ||
| raise typer.Exit(1) | ||
|
|
||
| resolved_path = Path(os.path.abspath(os.path.expanduser(candidate))).as_posix() |
There was a problem hiding this comment.
Reject relative saved paths when falling back in set-local
When --local-path is omitted, this command reuses entry.path and immediately runs abspath() on it. Legacy cloud entries can carry non-absolute placeholder paths (the config migration comments explicitly call out slug paths), so this silently rebases them to the current working directory and writes that to config/DB. In that scenario set-local can attach the project to an unintended directory without user confirmation; require an absolute fallback path (or force --local-path) before resolving.
Useful? React with 👍 / 👎.
| detached = run_with_cleanup(_detach_local_project_row(config, name)) | ||
|
|
There was a problem hiding this comment.
Persist config before deleting the local project row
The command deletes the local DB row before writing the updated config. If save_config fails (e.g., disk full/permissions), the project remains local in config but its DB row and indexed state are already removed, leaving the local project in a broken hybrid state until manual repair/reindex. This should be made atomic (or rollback the delete on save failure) so partial failure doesn’t corrupt local state.
Useful? React with 👍 / 👎.
set-local stores `path` via Path(...).as_posix() (matching bm project add and the rest of the codebase, which keep paths as POSIX strings). The test compared against str(Path), which on Windows produces backslashes and fails the equality check on the Windows CI runner. Use .as_posix() in the assertion so the test matches what the production code actually writes to config. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 4m 2s —— View job Code Review:
|
| Item | Status |
|---|---|
| Follows CLAUDE.md coding conventions | |
| Python 3.12+ type annotations | app_config untyped (patch ready) |
| SQLAlchemy 2.0 async patterns | ✅ |
| FastAPI / Typer conventions | ✅ |
| 100-character line length limit | ✅ |
| No commented-out code | ✅ |
| Unit tests for new functions | |
| 100% test coverage maintained | return True and update_path branches untested (patch ready) |
| Literate programming / Trigger-Why-Outcome comments | ✅ Excellent |
| DB changes / Alembic migrations | ✅ N/A |
| Local-first architecture preserved | ✅ |
| Windows path compatibility | ✅ Fixed in 28c49b6 |
| No emojis/unicode in console output | ✅ |
shutdown_db() assumption documented |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28c49b6aa4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| run_with_cleanup(_attach_local_project_row(config, name, resolved_path)) | ||
|
|
||
| config.set_project_mode(name, ProjectMode.LOCAL) | ||
| config.projects[name].workspace_id = None | ||
| config.projects[name].path = resolved_path |
There was a problem hiding this comment.
Save config before reattaching local project row
set-local recreates/updates the local DB project row before persisting the config change. If save_config fails (for example due to disk/permission errors), config still says the project is cloud-routed while the local DB row has already been reintroduced, recreating the same hybrid local+cloud state this change is trying to eliminate and requiring manual cleanup. Make this transition atomic (or rollback the DB write on save failure) so partial failures do not corrupt project state.
Useful? React with 👍 / 👎.
Address reviewer follow-ups on #775: - Add type annotations to the two CLI helpers (BasicMemoryConfig). - Note the CLI-only assumption inline at each shutdown_db() call. - Add two tests that exercise the previously uncovered branches: - test_set_local_update_path_when_row_exists hits repo.update_path. - test_set_cloud_removes_existing_db_row hits repo.delete on the detach helper and asserts the user-visible cleanup message. Signed-off-by: phernandez <paul@basicmachines.co>
|
Claude finished @phernandez's task in 2m 36s —— View job Code Review:
|
| Issue | Resolution |
|---|---|
Missing BasicMemoryConfig type annotation on both helpers |
Fixed in a42c6a2 — app_config: BasicMemoryConfig added, import added at line 29 |
Windows-incompatible str(new_path) in test assertions |
Fixed in 28c49b6 — both assertions now use new_path.as_posix() |
_detach_local_project_row return True branch never exercised |
Fixed in 8f26f15 — test_set_cloud_removes_existing_db_row seeds a DB row then calls set-cloud, asserting the "local index entry removed" message |
_attach_local_project_row update_path branch never exercised |
Fixed in 8f26f15 — test_set_local_update_path_when_row_exists calls set-local twice with different paths, verifying config reflects the second path |
shutdown_db() CLI-only assumption undocumented |
Fixed in a42c6a2 — both finally blocks carry the CLI-only comment |
One remaining concern (Codex P2 — judgment call)
set-local operation ordering could transiently recreate the hybrid state
project.py:1034 — _attach_local_project_row runs before config_manager.save_config():
run_with_cleanup(_attach_local_project_row(config, name, resolved_path)) # DB row created
config.set_project_mode(name, ProjectMode.LOCAL) # ← if save fails
config.projects[name].workspace_id = None # ↑ after this,
config.projects[name].path = resolved_path # ↑ config still says cloud
config_manager.save_config(config)If save_config fails (disk full, permissions) after the DB attach succeeds, config still shows mode=cloud while the local DB has a new row — exactly the local+cloud hybrid state this PR eliminates. Running set-local again with the same path recovers the situation idempotently, so it's not permanent. The analogous path in set-cloud (project.py:967) has a comment explaining the ordering was intentional; set-local could use the same. Alternatively, swapping the order (save config first, then attach DB row) would eliminate the regressed-bug window at the cost of a different broken state on failure (correct config, missing DB row — which sync would self-heal on next run).
This is a rare failure mode and both orderings are survivable. Mentioning it for visibility, not as a blocker.
Checklist
| Item | Status |
|---|---|
| Follows CLAUDE.md coding conventions | ✅ |
| Python 3.12+ type annotations | ✅ Fixed |
| SQLAlchemy 2.0 async patterns | ✅ |
| FastAPI / Typer conventions | ✅ |
| 100-character line length limit | ✅ |
| No commented-out code | ✅ |
| Unit tests for new functions | ✅ |
| 100% test coverage (all branches exercised) | ✅ Fixed |
| Literate programming / Trigger-Why-Outcome comments | ✅ Excellent |
| DB changes / Alembic migrations | ✅ N/A |
| Local-first architecture preserved | ✅ |
| On-disk file integrity | ✅ Explicitly not touched |
| Windows path compatibility | ✅ Fixed |
| No emojis/unicode in console output | ✅ |
shutdown_db() assumption documented |
✅ Fixed |
set-local operation ordering |
Summary: All blocking issues from prior reviews are resolved. The implementation is clean, idempotent, and well-tested. The one open item (operation ordering in set-local) is a rare-failure edge case with a recoverable path — worth a one-line comment if you'd like to document the tradeoff, but not a merge blocker.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8f26f159c7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if resolved_workspace_id: | ||
| config.projects[name].workspace_id = resolved_workspace_id | ||
| # Clear local path: source-of-truth for this project is now the cloud | ||
| config.projects[name].path = "" |
There was a problem hiding this comment.
Preserve local sync path during set-cloud cutover
Do not blank entry.path for every cloud cutover, because cloud projects with an existing local bisync copy rely on that path to stay eligible for local watchers/sync. After this line runs, local_sync_path may still be set but both cloud filters (services/initialization.py and sync/watch_service.py) check Path(entry.path).is_absolute() to decide whether to keep cloud projects, so rerunning set-cloud (for example to change workspace) can silently disable background sync/watch for that project’s local mirror.
Useful? React with 👍 / 👎.
|
Quick triage of the Codex review on P1 — preserve P2 — relative-path fallback in P2 — CI is green; merging. |
Summary
Per @groksrc's direction in #680,
set-cloudis now a one-way cutover instead of a routing toggle.Before this change,
set-cloudonly flippedmode = cloudin config and left the project's row in the local index DB alone — so_merge_projectscontinued to see the project on both sides and reportedsource: "local+cloud"with the oldlocal_pathstill populated. Confusing state, no clean way out without manually editing bothconfig.jsonandmemory.db.Changes
set-cloudnow drops the project's row from the local index DB and clearspathin config. On-disk note files are preserved — the user can keep, archive, or delete them as they like.set-localis the symmetric inverse. It accepts--local-pathand recreates the local DB row at the resolved path. If the prior path is still in config (e.g. runningset-localon a project that's already local),--local-pathmay be omitted and the existing path is reused._detach_local_project_row,_attach_local_project_row) are idempotent: runningset-cloudtwice doesn't error; runningset-localtwice updates the path if it changed and otherwise no-ops.Notes / behavior changes
set-local <name>worked on aset-cloud'd project even though that left the DB in a hybrid state. After this change, that command path errors out and asks for--local-path. This is intentional — the prior state was the bug.--local/--cloudrouting flags are unrelated to this; they remain a per-invocation override of routing, whileset-cloud/set-localchange the project's configured state.Test plan
test_set_cloud_clears_path— verifies configpathis blanked afterset-cloudtest_set_local_requires_path_after_set_cloud— regression for the exact scenario in the issue: re-binding to local must not silently defaulttest_set_local_success— full round trip with an explicit pathtest_set_local_clears_workspace_idupdated to pass--local-pathuv run pytest tests/cli/ --no-cov— 309 passedCloses #680
🤖 Generated with Claude Code