#7369·hive

[Bug]: atomic_write() raises PermissionError on Windows when the destination is briefly held open by a concurrent reader

Author: Prabal864Created Aug 15, 2026Updated Sep 9, 2026

Description

atomic_write() in core/framework/utils/io.py is the shared crash-safe write primitive used across the framework's entire persistence layer — checkpoints, conversations, sessions, tasks, sentinel state, skills, and more. On Windows, its final step (tmp_path.replace(path)) is not resilient to a very ordinary condition: another process or thread briefly having the destination file open for read at the same instant.

Location

python
# core/framework/utils/io.py
@contextmanager
def atomic_write(path: Path, mode: str = "w", encoding: str = "utf-8"):
    tmp_path = path.with_suffix(path.suffix + ".tmp")
    try:
        with open(tmp_path, mode, encoding=encoding) as f:
            yield f
            f.flush()
            os.fsync(f.fileno())
        tmp_path.replace(path)          # <-- fails on Windows if `path` is open elsewhere
    except BaseException:
        tmp_path.unlink(missing_ok=True)
        raise

Root Cause

Path.replace()os.replace() → Win32 MoveFileExW. On POSIX, renaming over an open file always succeeds (the old inode stays valid for existing descriptors). On Windows, a rename/delete over a file that has any open handle fails unless that handle was opened with FILE_SHARE_DELETE — which Python's default open() does not request. So any concurrent reader (even a quick, already-closing open(path).read()), an antivirus real-time scanner, a search indexer, or a second Hive process instance can cause the replace to fail.

Steps to Reproduce

Verified directly against the real utility function (not just raw os.replace):

python
import tempfile
from pathlib import Path
from framework.utils.io import atomic_write

d = tempfile.mkdtemp()
target = Path(d) / "checkpoint.json"
target.write_text("original", encoding="utf-8")

# Simulate a concurrent reader (e.g. load_checkpoint(), an AV scanner,
# a search indexer, or a second Hive process) briefly holding the file
# open at the exact moment a writer replaces it.
reader = open(target, "r", encoding="utf-8")
try:
    with atomic_write(target) as f:
        f.write("new content")
except OSError as e:
    print("atomic_write FAILED:", repr(e))
finally:
    reader.close()

Output on Windows 11 / Python 3.14:

atomic_write FAILED: PermissionError(13, 'Access is denied')

Blast Radius

atomic_write is imported in 19 files, including:

  • core/framework/storage/checkpoint_store.py
  • core/framework/storage/conversation_store.py — writes conversations/parts/*.json
  • core/framework/storage/session_store.py, session_summary.py
  • core/framework/tasks/store.py, reminders.py
  • core/framework/sentinel/store.py
  • core/framework/maintenance/retention.py, janitor.py
  • core/framework/agents/queen/queen_profiles.py, queen_tools_config.py, tools_ga_migration.py
  • core/framework/host/colony_tools_config.py
  • core/framework/orchestrator/orchestrator.py

Any of these can raise an unhandled PermissionError mid-save whenever a reader happens to overlap with a writer on Windows.

Relationship to #7239

I believe this is the underlying root cause of #7239 ("Queen conversation crashes on windows while two concurrent processes tries to access the same file", [WinError 32]). Its crash log (conversations\parts\0000000389.json) traces directly to conversation_store.py's use of this same atomic_write(). #7239's suggested fix — adding a lock between compact_preserving_structure and subscribe_reflection_triggers — would patch that one call site, but wouldn't protect the other 18 callers of atomic_write, nor guard against an external process (AV, indexer, a second Hive instance) holding the file open, since that's outside any in-process lock's reach. I'd suggest the fix belongs in the shared utility itself.

Not filing this as a duplicate of #7239 — flagging it as the broader, systemic root cause so a fix in atomic_write can be evaluated instead of (or alongside) a narrower single-call-site lock.

Expected Behavior

atomic_write should tolerate a transient sharing violation on Windows — e.g. retry the replace() a few times with a short backoff (the standard mitigation for this well-documented Windows filesystem behavior; used by e.g. pip, various config-writers, and git-for-windows internals) — rather than propagating PermissionError straight to the caller.

Environment

  • OS: Windows 11
  • Python: 3.14.4 (also applicable to any Python version — this is Win32 API behavior, not a Python version issue)

Additional Context

Happy to open a PR with a retry-based fix in atomic_write() plus a regression test (reproducing the exact scenario above) if this is a direction the maintainers want.