From d1c389405bd7f19d242d5feda1145406eae2a54c Mon Sep 17 00:00:00 2001 From: chengwudi1 Date: Tue, 27 Oct 2026 09:42:02 +0800 Subject: [PATCH] fix(core): shorten FileSessionStore atomic temp file path on Windows Fixes #9682. In FileSessionStore.set(), the temporary file was named . ile_path.name.uuid.tmp, unnecessarily appending 38 characters to the snapshot name. For long session IDs in deep directories, this caused atomic writes to exceed Windows MAX_PATH (260 characters) and raise FileNotFoundError, even though the final snapshot path was within limits. Use a compact, ID-independent temporary filename .uuid.tmp so temp paths remain short and never exceed snapshot path length. --- .../core/agent_framework/_sessions.py | 4 ++- .../packages/core/tests/core/test_sessions.py | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/python/packages/core/agent_framework/_sessions.py b/python/packages/core/agent_framework/_sessions.py index 3b3b73d178f..6e7af4e6592 100644 --- a/python/packages/core/agent_framework/_sessions.py +++ b/python/packages/core/agent_framework/_sessions.py @@ -2148,7 +2148,9 @@ async def set(self, session_id: str, session: AgentSession) -> None: def _write() -> None: with file_lock: - temp_path = file_path.with_name(f".{file_path.name}.{uuid.uuid4().hex}.tmp") + # Use a short, ID-independent temp filename so that long session IDs + # close to the MAX_PATH limit on Windows do not overflow when saving. + temp_path = file_path.with_name(f".{uuid.uuid4().hex}.tmp") try: temp_path.write_bytes(serialized) os.replace(temp_path, file_path) diff --git a/python/packages/core/tests/core/test_sessions.py b/python/packages/core/tests/core/test_sessions.py index 9e01176cc3f..4b3053aea69 100644 --- a/python/packages/core/tests/core/test_sessions.py +++ b/python/packages/core/tests/core/test_sessions.py @@ -1244,6 +1244,36 @@ async def test_set_replaces_atomically_without_leaving_temp_files(self, tmp_path temp_files = await asyncio.to_thread(lambda: [*tmp_path.glob("*.tmp"), *tmp_path.glob(".*.tmp")]) assert not temp_files + async def test_set_long_session_id_atomic_write_does_not_overflow_path(self, tmp_path: Path) -> None: + """Verify atomic set succeeds for maximum length session IDs close to MAX_PATH limits. + + Fixes issue #9682 where the temp file name included the full snapshot name plus 38 chars, + causing FileNotFoundError on Windows when the final path was within limits but the temp path overflowed. + """ + session_id = "s" * FileSessionStore.MAX_SESSION_ID_LENGTH + base_len = len(str(tmp_path)) + # Desired final snapshot path length around 245 (within 260 character Windows MAX_PATH limit) + # Snapshot filename is 128 ('s' * 128) + 5 ('.json') = 133 chars + target_dir = tmp_path + needed_sub_len = 245 - base_len - 134 + if needed_sub_len > 0: + target_dir = tmp_path / ("d" * needed_sub_len) + target_dir.mkdir(parents=True, exist_ok=True) + + store = FileSessionStore(target_dir) + session = AgentSession(session_id=session_id) + session.state["key"] = "value" + + await store.set(session_id, session) + + stored = await store.get(session_id) + assert stored is not None + assert stored.session_id == session_id + assert stored.state.get("key") == "value" + + temp_files = await asyncio.to_thread(lambda: [*target_dir.glob("*.tmp"), *target_dir.glob(".*.tmp")]) + assert not temp_files + async def test_corrupt_session_file_is_quarantined_and_retry_returns_none(self, tmp_path: Path) -> None: store = FileSessionStore(tmp_path) await store.set("session-1", AgentSession(session_id="session-1"))