Skip to content

fix(io): copy-fallback for atomic writes on cross-device (EXDEV) rename#2696

Merged
esengine merged 1 commit into
main-v2from
fix/exdev-rename-fallback
Jun 2, 2026
Merged

fix(io): copy-fallback for atomic writes on cross-device (EXDEV) rename#2696
esengine merged 1 commit into
main-v2from
fix/exdev-rename-fallback

Conversation

@esengine

@esengine esengine commented Jun 2, 2026

Copy link
Copy Markdown
Owner

Closes #2694

What

Make atomic file writes survive Windows encryption-software filter drivers (亿赛通/Esafenet and similar), which reject the temp-file → target rename with a cross-device link error (EXDEV) even when both paths are in the same directory.

Why

The session save, conversation-branch metadata, config save (reasonix setup / /mcp add / /thinking), and the desktop .env / session-title / display saves all write to a sibling temp file and then os.Rename it onto the destination. On machines with these drivers that rename fails, so the save fails.

Note on #2694: the report's /apply "edit blocks" trace is from the v1 (Node) line — its EXDEV: ... rename string and write-file-atomic temp names are libuv's. In v2 the write_file / edit_file / multi_edit tools already write in place (os.WriteFile), so the edit path was never affected. The same failure mode does exist in v2's rename-based atomic saves, which this fixes.

How

New fileutil.ReplaceFile(tmp, dest): renames as before, and only if the rename fails, copies the temp file's bytes onto the destination (re-applying the temp's mode so a 0600 config temp doesn't widen) and removes the temp — the copy path the drivers permit. The original rename error is surfaced only if the copy fallback also fails, so genuine failures aren't masked. Routed the five rename sites (agent/save.go, agent/branch.go, config/edit.go, desktop/dotenv.go, desktop/sessions.go) through it.

codegraph/install.go also renames, but that's a directory move during install (a different, recursive case) and out of scope here.

Tests

internal/fileutil unit tests cover the rename happy path and the copy fallback (full overwrite of a longer destination + temp removal + mode). Runs on all three OS legs including windows-latest. Desktop module builds and its session tests pass.

…evice link

Windows encryption-software filter drivers (e.g. 亿赛通/Esafenet) intercept the temp-file -> target rename that the session, branch, config, and desktop .env/title saves use, returning a cross-device link (EXDEV) even within one directory. The save then fails. Add fileutil.ReplaceFile: it renames as before and, only when that fails, copies the temp file's bytes onto the destination (preserving mode) and removes the temp — the path the encryption drivers allow. Route the rename sites through it. The edit/write tools already write in place, so they were never affected.
@github-actions github-actions Bot added the v2 Go rewrite (1.x) — main-v2 branch, active development label Jun 2, 2026
@esengine esengine merged commit c7b119e into main-v2 Jun 2, 2026
5 checks passed
@esengine esengine deleted the fix/exdev-rename-fallback branch June 2, 2026 07:24
SivanCola added a commit that referenced this pull request Jul 5, 2026
…ename failures

ReplaceFile fell back to copyOnto after exhausting its rename retries,
regardless of why the rename failed. copyOnto truncates dest in place,
so a reader racing the copy observes an empty or half-written file —
silently breaking the atomic-publication contract every AtomicWriteFile
caller relies on (session leases, credentials, plugin state, mcp.json).
CI caught it once the window lined up: TestStateLoadDuringSaveNeverSees
TornFile failed on windows-latest with 'unexpected end of JSON input'
after a transient dest lock outlived the ~0.7s retry budget and the
copy truncated the state file under the reader.

Split the failure classes. Transient locks (antivirus, search indexer,
a reader without delete sharing) now retry longer — 12 attempts, ~1.5s
of backoff — and then surface the rename error; a loud failure beats a
silently torn file. The cross-device class (ERROR_NOT_SAME_DEVICE /
EXDEV, reported by encryption-software filter drivers even for a
same-dir rename, #2696) still takes the copy fallback because rename is
structurally impossible there — but now immediately, instead of first
sleeping through a retry ladder that cannot succeed, which also cuts
~0.7s of latency from every save on such hosts.

The rename seam (renameFile) exists because neither failure class can
be provoked portably on a real filesystem; the new tests pin dest
integrity plus the surfaced error for the transient class, and the
immediate no-retry copy for the cross-device class.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: 安装亿赛通等加密软件时-/apply的跨磁盘rename指令会报错

1 participant