fix(utils): atomic_replace falls back to shutil on cross-fs EXDEV (#34252) - #34262
fix(utils): atomic_replace falls back to shutil on cross-fs EXDEV (#34252)#34262Bartok9 wants to merge 1 commit into
Conversation
…usResearch#34252) Fixes NousResearch#34252. atomic_replace() uses os.replace() (rename(2) underneath) which fails with OSError(errno=EXDEV, 'Invalid cross-device link') when the temp file and the target sit on different filesystems. This happens when ~/.hermes/ is symlinked to a different mount point — a common deployment shape on Linux VPS hosts where /home is small and the data lives on /mnt/data/. Impact (per reporter): - hermes config set / hermes model crash with [Errno 18] - Gateway fails to persist dedup state (feishu_seen_message_ids.json) silently - Channel directory state is lost on gateway restart - Users with cross-fs HERMES_HOME cannot use the agent at all Fix: catch OSError(EXDEV) and fall back to shutil.move (copy + unlink internally when crossing devices). Not strictly atomic across devices, but preserves the functional contract — the target ends up with the new content. Non-EXDEV OSErrors (permission denied, read-only filesystem, etc.) still propagate unchanged — the fallback is intentionally narrow. Adds 3 regression tests: - EXDEV fallback to shutil.move - Non-EXDEV errors still propagate (sanity check) - Happy path still uses os.replace (atomicity on same-fs preserved) All 11 tests in test_atomic_replace_symlinks.py pass (3 new + 8 existing). Co-authored-by: Cursor <cursoragent@cursor.com>
|
Confirming the deferral noted above — #33508 covers the same EXDEV failure with copy2+unlink and carries the broader test set (including the symlink-target case and end-to-end atomic_yaml_write). I've linked issue #34252 from #33508 so it closes on merge, with credit to @ccwssy for the root-cause analysis. This one can be closed as a duplicate unless #33508 stalls. |
|
Closing as superseded. Current |
Fixes #34252.
Problem
atomic_replace()usesos.replace()(rename(2)syscall) which fails withOSError(errno=EXDEV, 'Invalid cross-device link')when the temp file and the target sit on different filesystems. This happens when~/.hermes/is symlinked to a different mount point — a common deployment shape on Linux VPS hosts where/homeis small and the data lives on/mnt/data/.Reporter's impact
hermes config set/hermes modelcrash with[Errno 18]feishu_seen_message_ids.json) silentlyFix
Catch
OSError(EXDEV)and fall back toshutil.move(copy + unlink internally when crossing devices):Not strictly atomic across devices, but preserves the functional contract — the target ends up with the new content. Non-EXDEV
OSErrors (permission denied, read-only filesystem, etc.) still propagate unchanged — the fallback is intentionally narrow so it can't mask other failure modes.Tests
3 new tests in
test_atomic_replace_symlinks.py:test_atomic_replace_falls_back_to_shutil_on_exdevtest_atomic_replace_reraises_non_exdev_errorsEACCESetc. still propagatetest_atomic_replace_happy_path_still_uses_os_replaceos.replace(atomicity preserved on the 99% case)All 11 tests pass (3 new + 8 existing — including the symlink-preservation invariants from #16743 that this fix carefully preserves).
Credit to @ccwssy for the reporter's surgical root-cause analysis and proposed diff.
Co-authored-by: Cursor cursoragent@cursor.com