fix(config): delete thread dir through extended-length path to avoid WinError 145 (#8144)

Co-authored-by: Nan Gao <thunderggnn@gmail.com>
This commit is contained in:
ywy 2026-11-12 14:24:24 +08:00 committed by GitHub
parent b80191fad3
commit 98e8598adb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 36 additions and 5 deletions

View File

@ -60,6 +60,12 @@ This section accumulates work toward the **2.3.0** milestone
### Fixed
- **threads:** Deleting a conversation on Windows no longer fails with
`Failed to delete local thread data.` when the workspace holds a file whose
path exceeds `MAX_PATH`. The thread directory is now resolved through the
Windows extended-length namespace before removal.
([#8144](https://github.com/bytedance/deer-flow/pull/8144))
- **channels:** A WeCom group command that follows the bot mention now runs as a
command. WeCom keeps the mention inside the message text (`@RobotA /new`), and
the classifier required a bare leading `/`, so `/new` and `/status` sent in a

View File

@ -508,12 +508,18 @@ class Paths:
def delete_thread_dir(self, thread_id: str, *, user_id: str | None = None) -> None:
"""Delete all persisted data for a thread.
Idempotent: missing thread directories are ignored. A file the host marks
read-only is retried once after clearing that bit, because Windows refuses
to unlink it where POSIX only needs a writable parent; a file that stays
undeletable still raises.
Idempotent: missing thread directories are ignored. The tree is removed
through the Windows extended-length namespace so a nested file whose full
path exceeds ``MAX_PATH`` (for example a plugin's ``files/<sha256>.docx``
under a deep user/thread workspace) is still unlinked. Without the prefix
``shutil.rmtree`` leaves the long child behind and ``os.rmdir`` fails with
``WinError 145``.
A file the host marks read-only is retried once after clearing that bit,
because Windows refuses to unlink it where POSIX only needs a writable
parent; a file that stays undeletable still raises.
"""
thread_dir = self.thread_dir(thread_id, user_id=user_id)
thread_dir = extended_length_path(self.thread_dir(thread_id, user_id=user_id))
try:
shutil.rmtree(thread_dir)
except FileNotFoundError:

View File

@ -307,6 +307,25 @@ class TestEnsureAndDeleteWithUserId:
paths.delete_thread_dir("t1", user_id="u1")
assert paths.thread_dir("t1", user_id="u1").exists()
def test_delete_thread_dir_uses_extended_length_path(self, paths: Paths, monkeypatch):
"""``shutil.rmtree`` receives the path resolved through ``extended_length_path``.
The fix for the ``MAX_PATH`` deletion failure is a single line: resolve
the thread directory through ``extended_length_path`` before removing it.
That call is a no-op on POSIX, so this pins the wiring by patching the
resolver to return a marker and asserting ``shutil.rmtree`` receives it.
"""
from deerflow.config import paths as paths_module
marker = object()
monkeypatch.setattr(paths_module, "extended_length_path", lambda _path: marker)
received = []
monkeypatch.setattr(paths_module.shutil, "rmtree", lambda path, *args, **kwargs: received.append(path))
paths.ensure_thread_dirs("t1", user_id="u1")
paths.delete_thread_dir("t1", user_id="u1")
assert received == [marker]
def test_ensure_thread_dirs_legacy_still_works(self, paths: Paths):
paths.ensure_thread_dirs("t1")
assert paths.sandbox_work_dir("t1").is_dir()