hermes - ✅(Solved) Fix Bug: memory tool fails with Invalid cross-device link (EXDEV) [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#17313Fetched 2026-04-30 06:48:28
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
0
Participants
Timeline (top)
labeled ×4cross-referenced ×2

Error Message

The memory tool fails to write/update memory when the temp file and destination are on different filesystems/mounts. The implementation uses os.rename() which does not work across device boundaries (EXDEV error).

Error Output

Error executing tool: Failed to write memory file /root/.hermes/memories/MEMORY.md: [Errno 18] Invalid cross-device link: /root/.hermes/memories/.mem_wre8n6ee.tmp -> /opt/hermes-vault/hermes-memory/MEMORY.md

Root Cause

The code attempts to rename .mem_*.tmp from one mount to another. These paths are on different filesystems/devices, and os.rename() cannot handle cross-device moves.

Fix Action

Fixed

PR fix notes

PR #17322: fix(utils): fall back when atomic_replace crosses filesystems (#17313)

Description (problem / solution / changelog)

Summary

  • atomic_replace now falls back to a sibling-temp + same-fs replace when os.replace raises OSError(EXDEV), so symlink-following writes survive when the resolved real path is on a different filesystem than the caller's temp.
  • Fixes the memory tool's [Errno 18] Invalid cross-device link failure on Docker / NAS / managed-vault setups (#17313). Same fix transparently covers every other call site already routed through atomic_replacetools/skill_manager_tool.py, tools/skills_sync.py, utils.atomic_json_write, and utils.atomic_yaml_write.

The bug

utils.atomic_replace was introduced in #16743 to keep symlinked deployment files (e.g. ~/.hermes/config.yaml → a profile package) intact across atomic writes. It calls os.path.realpath(target) and then os.replace(tmp, real_path).

When the symlink crosses a filesystem boundary — common in #17313's setup, where ~/.hermes/memories/MEMORY.md is symlinked into /opt/hermes-vault/hermes-memory/ on a separate mount — the temp file lives on one device while the real file lives on another. os.replace cannot move inodes between filesystems, so it raises OSError(EXDEV):

Failed to write memory file /root/.hermes/memories/MEMORY.md:
  [Errno 18] Invalid cross-device link:
  /root/.hermes/memories/.mem_wre8n6ee.tmp -> /opt/hermes-vault/hermes-memory/MEMORY.md

Every call site that reaches atomic_replace through a symlink to a different mount hits the same path: memory writes, skill manager edits, JSON/YAML config writes.

The fix

Catch OSError(errno=EXDEV) from the inner os.replace and:

  1. tempfile.mkstemp a sibling temp in the real target's directory (same filesystem as the destination).
  2. shutil.copyfile the original temp's content onto the sibling.
  3. os.replace(sibling_tmp, real_path) — now within a single filesystem, so the swap is atomic from any reader's perspective.
  4. os.unlink the original cross-device temp.

If the inner os.replace itself fails, the sibling temp is cleaned up before re-raising, so we don't leak .atomic_xdev_*.tmp files into the user's vault.

Same-filesystem writes hit the original fast path unchanged. Only EXDEV triggers the fallback — every other OSError propagates exactly as before, preserving existing error-handling tests in tests/gateway/test_weixin.py (which monkeypatch utils.os.replace to raise non-EXDEV errors).

Contract Protected

atomic_replace(tmp, target) invariant: writes the contents of tmp to the real file behind target, atomically from a reader's perspective, regardless of which filesystem tmp was created on.

Input scenarioPre-fix behaviorPost-fix behavior
Same-fs, regular fileAtomic os.replaceAtomic os.replace (unchanged)
Same-fs, symlinked targetAtomic os.replace on real pathAtomic os.replace on real path (unchanged)
Cross-fs, symlinked targetOSError(EXDEV) propagatesAtomic replace via sibling temp
Inner replace fails (e.g. EACCES)n/a (only triggered by fallback)Sibling temp unlinked, original error re-raised
OSError != EXDEV from os.replacePropagatesPropagates (unchanged)

Test plan

  • tests/test_atomic_replace_symlinks.py — 4 new cases on the helper:
    • test_atomic_replace_recovers_from_exdev — reporter's exact error path
    • test_atomic_replace_exdev_with_symlink_preserves_link — symlink + cross-fs, link survives
    • test_atomic_replace_propagates_non_exdev_oserror — non-EXDEV OSError still raises
    • test_atomic_replace_exdev_cleans_sibling_on_replace_failure — no leftover .atomic_xdev_* on inner-replace failure
  • tests/tools/test_memory_tool.py::TestMemoryToolCrossDeviceWrite — integration: MemoryStore.add round-trips through disk when the first os.replace raises EXDEV, with no leftover .mem_*.tmp or .atomic_xdev_*.tmp in the memories dir.
  • Adjacent suites confirmed clean: test_atomic_replace_symlinks.py (16 tests, all pass), tests/tools/test_memory_tool.py (30 tests), tests/hermes_cli/test_config.py (52 tests, exercises atomic_yaml_write), tests/gateway/test_weixin.py (42 tests, mocks utils.os.replace with non-EXDEV errors).
  • Regression guard: 4 of the 5 new tests fail on clean origin/main (58a6171bf) with the reporter's exact error: RuntimeError: Failed to write memory file …: [Errno 18] Invalid cross-device link. The fifth (non-EXDEV propagation) passes on baseline because that path was already correct.

Related

  • Fixes #17313
  • Builds on #16743 (introduced atomic_replace symlink-following) — that PR made the helper symlink-aware but didn't account for the symlink resolving to a different filesystem.
  • No active competing PR. No prior closed PR by me on atomic_replace / EXDEV / memory-tool atomic-write paths.

Changed files

  • tests/test_atomic_replace_symlinks.py (modified, +185/-0)
  • tests/tools/test_memory_tool.py (modified, +55/-0)
  • utils.py (modified, +68/-1)

PR #17357: fix: handle EXDEV in atomic_replace for cross-device memory writes

Description (problem / solution / changelog)

What does this PR do?

atomic_replace() in utils.py uses os.replace() which fails with EXDEV (errno 18) when the temp file and resolved target path are on different filesystems. This breaks the memory tool for users with Docker volumes, NFS mounts, or symlinked memory directories.

The fix tries os.replace() first (preserves atomicity on same filesystem), then falls back to shutil.move() on EXDEV.

Related Issue

Fixes #17313

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • utils.py: wrap os.replace() in try/except, catch errno.EXDEV, fall back to shutil.move() (+6 lines)

How to Test

  1. Create a symlink from ~/.hermes/memories/MEMORY.md to a path on a different filesystem (e.g., a mounted Docker volume or NFS share)
  2. Run any memory write operation (e.g., fact_store(action="add", ...))
  3. Before fix: [Errno 18] Invalid cross-device link
  4. After fix: write succeeds silently

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • Cross-platform: fix uses errno.EXDEV (POSIX-standard) and shutil.move() (cross-platform)

Changed files

  • utils.py (modified, +9/-1)

Code Example

Error executing tool: Failed to write memory file /root/.hermes/memories/MEMORY.md: [Errno 18] Invalid cross-device link: /root/.hermes/memories/.mem_wre8n6ee.tmp -> /opt/hermes-vault/hermes-memory/MEMORY.md

---

import shutil

# Instead of: os.rename(tmp_path, dest_path)
# Use:
shutil.move(tmp_path, dest_path)
RAW_BUFFERClick to expand / collapse

Bug Description

The memory tool fails to write/update memory when the temp file and destination are on different filesystems/mounts. The implementation uses os.rename() which does not work across device boundaries (EXDEV error).

Error Output

Error executing tool: Failed to write memory file /root/.hermes/memories/MEMORY.md: [Errno 18] Invalid cross-device link: /root/.hermes/memories/.mem_wre8n6ee.tmp -> /opt/hermes-vault/hermes-memory/MEMORY.md

Root Cause

The code attempts to rename .mem_*.tmp from one mount to another. These paths are on different filesystems/devices, and os.rename() cannot handle cross-device moves.

Expected Behavior

Memory should be written successfully even when the source temp directory and destination path are on different filesystems.

Suggested Fix

Replace the atomic rename with a cross-device-safe move:

import shutil

# Instead of: os.rename(tmp_path, dest_path)
# Use:
shutil.move(tmp_path, dest_path)

Severity

High — Memory is a core feature. Users with custom mounts, Docker volumes, or multi-filesystem setups will consistently hit this bug.

extent analysis

TL;DR

Replace os.rename() with shutil.move() to enable cross-device moves and fix the memory writing issue.

Guidance

  • Identify all locations where os.rename() is used to move files across different filesystems or mounts.
  • Replace these instances with shutil.move() to ensure cross-device compatibility.
  • Verify that the new implementation works correctly by testing memory writing with temp files and destinations on different filesystems.
  • Consider adding error handling for cases where shutil.move() might still fail, such as permission issues.

Example

import shutil
import os

# Assuming tmp_path and dest_path are defined
try:
    shutil.move(tmp_path, dest_path)
except OSError as e:
    # Handle potential errors, such as permission denied
    print(f"Error moving file: {e}")

Notes

This fix assumes that shutil.move() is available and compatible with the existing codebase. If shutil is not available, an alternative cross-device move implementation may be needed.

Recommendation

Apply workaround: Replace os.rename() with shutil.move() to fix the memory writing issue, as it provides a straightforward and compatible solution for cross-device file moves.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix Bug: memory tool fails with Invalid cross-device link (EXDEV) [2 pull requests, 1 participants]