Skip to content

fix: handle cross-device rename in atomic_json_write#51

Open
on22s wants to merge 1 commit into
Finrandojin:devfrom
on22s:fix/atomic-write-cross-device
Open

fix: handle cross-device rename in atomic_json_write#51
on22s wants to merge 1 commit into
Finrandojin:devfrom
on22s:fix/atomic-write-cross-device

Conversation

@on22s

@on22s on22s commented Jun 4, 2026

Copy link
Copy Markdown

Summary

atomic_json_write() in utils.py uses os.replace() to rename a temp file over the target. On Linux, os.replace() raises OSError(EXDEV, 'Invalid cross-device link') when the source and destination are on different filesystems — this affects bind-mounted project directories, NAS shares, and any setup where /tmp is on a different device than the project root.

The existing retry loop only caught Windows file-locking errors (errno 5 / "Access is denied"), so EXDEV was re-raised on the first attempt and the write failed permanently. Since chunks.json, voice_config.json, and LoRA manifests all go through atomic_json_write, this silently broke chunk saves and voice config saves on affected systems.

Fix: Add an EXDEV branch that falls back to shutil.move(), which performs a copy-then-delete when os.rename() is unavailable across device boundaries.

Details

  • errno.EXDEV is defined on all platforms Python supports (value 18; maps to ERROR_NOT_SAME_DEVICE on Windows)
  • shutil.move() is cross-platform
  • The fallback is not atomic, but avoids a hard failure — data is not lost
  • No behaviour change on same-device paths (the common case)

Test plan

  • python test_api.py — 55 passed, 0 failed, 13 skipped
  • Verify chunk saves and voice config saves still work normally on a standard setup

🤖 Generated with Claude Code

os.replace() raises OSError(EXDEV) when the source and destination are
on different filesystems (Linux bind mounts, NAS shares, tmpfs-backed
temp dirs). The retry loop only caught Windows file-locking errors
(errno 5 / "Access is denied"), so EXDEV was re-raised immediately and
every write to an affected path silently failed — chunks.json,
voice_config.json, and LoRA manifests all go through this path.

Add an EXDEV case that falls back to shutil.move(), which copies then
deletes when os.rename() is unavailable across device boundaries.
The fallback is not atomic, but avoids a hard failure in environments
where the project directory is on a different filesystem than /tmp.

errno.EXDEV is defined on all platforms Python supports (value 18;
maps to ERROR_NOT_SAME_DEVICE on Windows). shutil.move() is likewise
cross-platform. No behaviour change on same-device paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

1 participant