Skip to content

fix(tui): retry writeJsonAtomic on transient EPERM rename collisions on Windows #61119

Open

Description

@Bearmancer

Description

In packages/tui/src/util/persistence.ts, writeJsonAtomic creates a temporary file and replaces the destination using rename(temporary, filePath).

On Windows NTFS, atomic renames fail immediately with Error: EPERM: operation not permitted when another handle (such as anti-virus, indexing, or a concurrent async tick) holds an open handle or is replacing the destination file. Because packages/tui/src/context/local.tsx calls void writeJsonAtomic(filePath, ...), this rejection is unhandled and logs:

level=ERROR message="unhandled rejection" cause="Error: EPERM: operation not permitted, rename '.../session.json.<pid>.<uuid>.tmp' -> '.../session.json'" role=cli

When this occurs in bursts (e.g., during startup or event stream reconnects), the pending session.json state updates are lost.

Adding a short retry loop with exponential backoff for transient EPERM / EBUSY rename errors on win32 prevents lost writes.

Plugins

none

OpenCode version

2.2.5 (dev)

Steps to reproduce

  1. On Windows, run OpenCode in an environment where multiple state writes occur in quick succession.
  2. Check ~/.local/share/opencode/log/opencode.log for unhandled rejections during startup.
  3. Observe concurrent rename '.../session.json.<pid>.<uuid>.tmp' -> '.../session.json' throwing EPERM.

Screenshot

Not applicable (filesystem concurrency and unhandled rejection issue).

Operating System

Windows 11, Windows 10

Terminal

Windows Terminal, PowerShell

Activity

  1. opencodejr commented on Nov 2, 2026

    @opencodejr

    Thanks for the clear report. I couldn't reproduce this here because it needs Windows file locking. This sandbox is Linux, where renaming over a file another process has open always succeeds, so the EPERM can't happen. A maintainer with a Windows machine will need to confirm it.

    On the latest dev (8bca532), writeJsonAtomic in packages/tui/src/util/persistence.ts still renames the temporary file over session.json only once, with no retry. If the rename fails, it deletes the temporary file and passes the error on. The session.json write in packages/tui/src/context/local.tsx doesn't catch that error, which fits the unhandled rejection and the lost write you're seeing.

    Related:

  2. Bearmancer commented on Nov 2, 2026

    @Bearmancer
    Author

    @opencodejr Thanks for confirming the mechanism on dev (8bca532).

    Indeed, on Windows NTFS, file handles held by anti-virus scanners, Windows search indexers, or rapid concurrent async ticks cause atomic renames to fail with EPERM rather than waiting. Adopting the exact retry-with-backoff pattern already implemented for the desktop app's state persistence in #51470 / #54530 will make writeJsonAtomic resilient on win32.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions