Skip to content

fix(repair): use os.rename for in-place archive, not shutil.move - #1945

Merged
igorls merged 1 commit into
MemPalace:developfrom
KeilerHirsch:fix/repair-atomic-archive-rename
Jul 8, 2026
Merged

fix(repair): use os.rename for in-place archive, not shutil.move#1945
igorls merged 1 commit into
MemPalace:developfrom
KeilerHirsch:fix/repair-atomic-archive-rename

Conversation

@KeilerHirsch

Copy link
Copy Markdown
Contributor

Problem

rebuild_from_sqlite's in-place archive step (--archive-existing) uses shutil.move(dest_palace, archive_path). When os.rename fails (cross-filesystem, or on Windows when a file inside the tree is held open by another process), shutil.move falls back to copytree + rmtree. That rmtree deletes the live palace file-by-file — and if it hits a locked file partway through, the palace is left partially deleted next to a partial (or empty) archive copy.

Real-world repro

Hit this live, twice, on two separate nights (Windows 11, 2026-07-05 and 2026-07-06): running mempalace repair --mode from-sqlite --yes --archive-existing while an MCP server / a detached mine process held palace/<segment-uuid>/data_level0.bin open. Both times:

PermissionError: [WinError 5] Zugriff verweigert: '...palace' -> '...palace.pre-rebuild-...'
...
PermissionError: [WinError 32] Der Prozess kann nicht auf die Datei zugreifen, da sie von einem anderen Prozess verwendet wird: '...\data_level0.bin'

The palace survived only because the specific locked files happened to be ones rmtree couldn't unlink — a different lock pattern (a lock on whichever file rmtree reaches first) would have lost data with no way back, since the archive itself is also incomplete at that point.

Fix

os.rename instead of shutil.move for this step. It's atomic on both platforms this matters on (POSIX rename(2), Windows MoveFileEx) — either fully succeeds or fails touching nothing. Catch the failure and print actionable guidance (close the process holding the lock, retry) instead of a raw traceback or a silently mangled palace.

Testing

  • New regression test test_rebuild_from_sqlite_in_place_archive_failure_leaves_palace_untouched (monkeypatches os.rename to raise, matching the real Windows error) — asserts the palace directory and chroma.sqlite3 are byte-for-byte untouched and no archive dir is created.
  • Full tests/test_repair.py: 91 passed, 0 failed.

Related to the general lock-contention family (#1908, #1888) but this is specifically about the archive-move fallback behavior, not the peer-writer lock itself.

shutil.move's fallback for a failed os.rename is copytree + rmtree. On
Windows, when any file inside the palace is held open by another
process (a live MCP server, a running mine, another harness), the
rename fails and shutil.move falls back to deleting the live palace
file-by-file via rmtree -- which itself then fails partway through on
the first locked file, leaving the palace partially gutted next to a
partial (or empty) archive copy.

Reproduced live twice (Windows 11, 2026-07-05 and 2026-07-06): running
`mempalace repair --mode from-sqlite --yes --archive-existing` while
an MCP server / detached mine held palace/*/data_level0.bin open threw
mid-rmtree in both cases. The palace itself survived only because the
specific locked files could not be unlinked -- a different lock
pattern (e.g. a lock on a file rmtree reaches first) would have lost
data with no way back.

os.rename is atomic on both platforms it matters on (POSIX rename(2),
Windows MoveFileEx) -- it either fully succeeds or fails without
touching anything. Catch the failure and abort cleanly with actionable
guidance instead of a raw traceback.
@igorls
igorls merged commit 6318868 into MemPalace:develop Jul 8, 2026
8 checks passed
@igorls igorls mentioned this pull request Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants