[Bug]: atomic_write() raises PermissionError on Windows when the destination is briefly held open by a concurrent reader
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
# 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)
raiseRoot 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):
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.pycore/framework/storage/conversation_store.py— writesconversations/parts/*.jsoncore/framework/storage/session_store.py,session_summary.pycore/framework/tasks/store.py,reminders.pycore/framework/sentinel/store.pycore/framework/maintenance/retention.py,janitor.pycore/framework/agents/queen/queen_profiles.py,queen_tools_config.py,tools_ga_migration.pycore/framework/host/colony_tools_config.pycore/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.
Source: aden-hive/hive