Skip to content

fix(cli): refuse mm uninstall --force on Windows when writer is alive#756

Merged
memtomem merged 1 commit intomainfrom
fix/730-uninstall-windows-contract
May 3, 2026
Merged

fix(cli): refuse mm uninstall --force on Windows when writer is alive#756
memtomem merged 1 commit intomainfrom
fix/730-uninstall-windows-contract

Conversation

@memtomem
Copy link
Copy Markdown
Owner

@memtomem memtomem commented May 3, 2026

Closes #730.

Summary

  • POSIX --force semantics rely on unlink-while-openshutil.rmtree succeeds with a live SQLite writer because the directory entry vanishes while the inode lives until the fd closes.
  • Windows refuses to unlink files held by an open handle (WinError 32), so the previous behavior crashed mid-_delete_inventory and left the state directory half-wiped.
  • This PR pins the contract to option (a) from the issue: refuse cleanly with a Windows-specific message instead of attempting a partial wipe. Exit code 2, mirroring the existing refusal path.
  • The trailing pass --force to override advice in the existing not-force refusal block is also dropped on Windows (it would just refuse again), and lsof / ps aux hints are replaced with handle.exe / Task Manager / Get-Process so the guidance is actionable on each platform.
  • The previously-skipped test_force_overrides_db_lock is replaced with a POSIX/Windows pair. The Windows variant seeds a sentinel non-DB file (config.json + memories/) and asserts they survive — proving the refusal fired before _delete_inventory ran, not after a partial wipe.

Out of scope (orthogonal follow-up)

Q2 from the issue — transactional wipe (stage to a sibling dir + atomic rename on success) is intentionally NOT in this PR. It helps even on POSIX (mid-rmtree perm/FS errors leave half-gone state today), but bundling it would muddle the contract decision. Will file a separate issue/PR.

Test plan

  • uv run ruff check + ruff format --check (clean)
  • uv run mypy packages/memtomem/src/memtomem/cli/uninstall_cmd.py (clean)
  • uv run pytest packages/memtomem/tests/test_uninstall_cmd.py -m "not ollama" — 28 passed, 1 skipped (the Windows-only test, as expected on macOS)
  • Windows CI lane: new test_force_refuses_on_windows_when_writer_alive runs and passes; test_force_overrides_db_lock_posix is skipped; test_refuses_when_writer_holds_lock passes with the platform-conditional lsof/handle.exe assertion.

🤖 Generated with Claude Code

…ve (closes #730)

POSIX `--force` semantics rely on unlink-while-open — `shutil.rmtree`
succeeds even with a live SQLite writer because the directory entry is
gone while the inode lives until the fd closes. Windows refuses to
unlink files held by an open handle (WinError 32), so the previous
behavior crashed mid-`_delete_inventory` and left the state directory
half-wiped.

Pin the contract to option (a) from the issue: refuse cleanly with a
Windows-specific message instead of attempting a partial wipe. The
trailing `pass --force to override` advice in the existing not-force
refusal block is also dropped on Windows (it would just refuse again),
and `lsof` / `ps aux` hints are replaced with `handle.exe` / Task
Manager / Get-Process so the guidance is actionable on each platform.

Replace the Windows-skipped `test_force_overrides_db_lock` with a
POSIX/Windows pair. The Windows variant seeds a sentinel non-DB file
(config.json + memories/) and asserts they survive — proving the
refusal fired before `_delete_inventory` ran, not after a partial wipe.

Q2 from the issue (transactional wipe via staging dir + atomic rename)
is orthogonal and tracked as a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@memtomem memtomem merged commit 35a4d42 into main May 3, 2026
9 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators May 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows: contract for mm uninstall --force against a live writer is undefined

1 participant