Skip to content

Python: [Bug]: On Windows without long paths, FileSystemAgentFileStore reports FileNotFoundError for long-but-legal keys #10141

Closed

Description

@simin-yuan

Observed Behavior

On Windows with long paths disabled (LongPathsEnabled = 0, the default on Windows 10/11), FileSystemAgentFileStore rejects a key that InMemoryAgentFileStore accepts, and reports the rejection as FileNotFoundError (errno 2, WinError 206 = ERROR_FILENAME_EXCED_RANGE).

Concretely, tests/core/test_harness_file_access.py::test_filesystem_store_matches_in_memory_complexity_bound fails on such a machine. The test's docstring states the intent: "Both backends refuse the same over-bound path with the same errno, so backend substitution is not observable on the bound either."

What I measured directly (temporary root, no pytest):

key FileSystemAgentFileStore InMemoryAgentFileStore
8000 segments (over any bound) OSError, errno 38 (ENAMETOOLONG) OSError, errno 38
200 segments (~400 chars, legal on POSIX) FileNotFoundError, errno 2, winerror 206 accepted (write + read ok)

So the "over-bound" half of the test passes on Windows: both backends agree on ENAMETOOLONG for a clearly absurd key. The half that fails is the second one, where the test writes a long-but-plausible key and expects both backends to accept it.

Absolute-path sweep with an 85-character root (keys of "d/" * n + "f"):

key chars approx. absolute path filesystem backend in-memory backend
161 241 ok ok
241 321 FileNotFoundError, errno 2, winerror 206 ok
401 481 FileNotFoundError, errno 2, winerror 206 ok

Expected Behavior

The inconsistency at the platform bound is inherent — Windows simply has a shorter limit than POSIX — but two things look wrong:

  1. A path that is merely too long is reported as FileNotFoundError, which reads as "the file is missing" and sends callers looking for a bug in their own path handling. OSError with errno.ENAMETOOLONG (which the same store already produces for the 8000-segment case) would be accurate and consistent.
  2. The platform difference between the two backends is not documented, yet the test asserts it does not exist.

Reasonable fixes, in the order I would pick them: map ERROR_FILENAME_EXCED_RANGE to ENAMETOOLONG on the write/read path; document the Windows bound on FileSystemAgentFileStore; scope the test's second half to platforms where both backends agree (or parameterize it on the host's actual limit).

Why CI does not catch this

python-tests.yml runs the suite on windows-latest. GitHub's Windows runner image sets LongPathsEnabled = 1 when the image is built (actions/runner-images, images/windows/scripts/build/Configure-BaseImage.ps1), so CI does not reproduce the default end-user configuration. A stock Windows 11 install ships with LongPathsEnabled = 0, which is where this test fails.

Steps to Reproduce

  1. On a Windows machine, confirm the setting:
reg query "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled
# LongPathsEnabled    REG_DWORD    0x0
  1. Run the existing test:
uv run pytest tests/core/test_harness_file_access.py::test_filesystem_store_matches_in_memory_complexity_bound -q
  1. Or probe directly:
import asyncio, tempfile
from agent_framework._harness._file_access import FileSystemAgentFileStore, InMemoryAgentFileStore

async def main() -> None:
    fs = FileSystemAgentFileStore(tempfile.mkdtemp())
    mem = InMemoryAgentFileStore()
    key = "/".join(["d"] * 200 + ["f"])  # ~400 chars
    await mem.write(key, "x")
    try:
        await fs.write(key, "x")
    except OSError as exc:
        print(type(exc).__name__, exc.errno, getattr(exc, "winerror", None))
        # FileNotFoundError 2 206

asyncio.run(main())

Package Versions

agent-framework-core: 1.24.0 (current main, commit e929275)

Python Version

Python 3.14.9

Operating System

Windows

Regression

Unknown

Additional Context

  • I did not run this on Linux. On POSIX the limit is per path segment rather than ~260 characters total, so a 400-character key with 200 segments is accepted there, which is why the test passes on CI.
  • I did not run this on CI; the LongPathsEnabled = 1 claim about the runner image comes from the runner-images build script, not from a local check.
  • Practical impact: a store rooted at a normal user directory (C:\Users\<name>\AppData\Local\..., ~85 characters here) plus a key of ~200 characters reaches the bound. The caller sees FileNotFoundError.
  • AI assistance: an AI assistant helped draft this report. I ran the reproduction and the test on the machine described above and verified the numbers and the error text.

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 6, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Nov 6, 2026
  3. github-actions commented on Nov 6, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    python/packages/core/agent_framework/_harness/_file_access.py::FileSystemAgentFileStore._write_file_sync propagates FileNotFoundError(errno=2, winerror=206) unchanged when Windows rejects an absolute path exceeding the legacy limit. Minimal repro: inject that exception from Path.mkdir during FileSystemAgentFileStore.write() or run the existing complexity-bound test on Windows with LongPathsEnabled=0.

    • Failing test: python/packages/core/tests/core/test_harness_file_access.py::test_filesystem_store_normalizes_windows_path_too_long_error
    • Files examined: python/packages/core/agent_framework/_harness/_file_access.py, python/packages/core/tests/core/test_harness_file_access.py, python/packages/core/pyproject.toml
    • Tests run: test_filesystem_store_matches_in_memory_complexity_bound, test_filesystem_store_normalizes_windows_path_too_long_error
    • Reported version: 1.24.0
    • Current version: 1.24.0
  4. added
    harness[Issues, PRs], Target: harness-level items
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Nov 6, 2026
  5. removed their assignment
    on Nov 6, 2026
  6. removed
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    harness[Issues, PRs], Target: harness-level items
    on Nov 6, 2026
  7. added
    harness[Issues, PRs], Target: harness-level items
    needs-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to process
    on Nov 6, 2026
  8. westey-m commented on Nov 6, 2026

    @westey-m
    Collaborator

    /plan

  9. github-actions commented on Nov 6, 2026

    @github-actions
    Contributor

    Summary

    On Windows with long paths turned off, FileSystemAgentFileStore.write() fails with FileNotFoundError (errno 2, WinError 206) when the full path is longer than about 260 characters. InMemoryAgentFileStore accepts the same key. The reporter wants the error to say "name too long", and wants the difference documented.

    What I confirmed in the code (read only, nothing run)

    • _write_file_sync in _file_access.py calls mkdir(parents=True) and then os.open(...). Both can raise the Windows "path too long" error. Only FileExistsError, PermissionError and ELOOP are handled. Anything else propagates unchanged, so callers see FileNotFoundError.
    • The store already raises OSError(errno.ENAMETOOLONG, ...) for paths past its internal size bound (around line 351). So ENAMETOOLONG is already the framework's way to say "path too long".
    • read() calls full_path.is_file() first. As far as I know, this returns False on error rather than raising, so a too-long read likely returns None ("file not found"). I did not verify this on Windows.
    • test_filesystem_store_matches_in_memory_complexity_bound writes a 200-segment key to both stores. Its docstring promises both backends behave the same. That cannot hold when the OS limit is lower than the in-memory limit.
    • I could not check the CI claim about LongPathsEnabled = 1 on the runner image. The automated triage comment says it reproduced the failure, but I did not inspect its test.

    Recommendation

    Code change plus documentation, with a small test adjustment. This is a small, low-risk fix. It does not change which paths are accepted. It only makes the error accurate. The Windows limit itself is an OS setting, so the two stores will still differ at that limit. That should be documented rather than "fixed".

    Existing workarounds are to enable Windows long paths or use a shorter root directory. Neither helps callers who see a misleading error.

    Proposed plan (proposal only; no changes made)

    1. Normalize the error. In _write_file_sync, wrap the mkdir and os.open calls. If the error is winerror == 206 (ERROR_FILENAME_EXCED_RANGE), raise OSError(errno.ENAMETOOLONG, "File name too long", path). Keep the original error as the cause. Use getattr(exc, "winerror", None) so the code stays safe on Linux and macOS.
    2. Fix the read path. Decide whether a too-long path in read() (and delete, list and similar) should raise ENAMETOOLONG or keep returning None. Raising matches the in-memory store's behavior for over-bound paths. Check each operation, not just write. I suggest the same normalization wherever a path is opened.
    3. Document the limit. Add a note to the FileSystemAgentFileStore docstring. On Windows without long-path support the total path (root plus key) is limited to about 260 characters, and the in-memory store has no such limit.
    4. Adjust the test. Keep the 8000-segment check, which passes everywhere. Make the "within bound" half conditional so it does not fail on a machine with long paths off. For example, skip it or shorten the key on Windows when long paths are disabled, or assert ENAMETOOLONG there.
    5. Add a unit test that runs everywhere. Patch Path.mkdir / os.open to raise FileNotFoundError with winerror=206, and assert the store raises OSError with ENAMETOOLONG. This covers the bug on any CI host, since CI has long paths enabled.
    6. Confirm resolution. The new test passes on all platforms. The existing complexity-bound test passes on a Windows machine with LongPathsEnabled = 0. The reporter's 200-segment probe now prints ENAMETOOLONG.

    Assumption: the winerror value is only set on Windows. I have not tested any of this on Windows.

    Generated by Issue Planning for #10141 · copilot · auto · ~18 AIC · ⌖ ~1 AIC · ⊞ 4.7K · ◷

  10. simin-yuan commented on Nov 6, 2026

    @simin-yuan
    ContributorAuthor

    Patch and tests are ready on my fork. Windows 11 with LongPathsEnabled = 0 reproduces
    the failure; after the change the same 401-character key raises OSError(ENAMETOOLONG)
    instead of FileNotFoundError(winerror 206). It normalizes WinError 206 at the mkdir and
    open call sites in _write_file_sync, documents the limit on the store, and makes the
    within-bound half of test_filesystem_store_matches_in_memory_complexity_bound expect
    ENAMETOOLONG on a host with long paths disabled. read() is untouched, as noted above.

    I have not opened a PR since the issue has no ready-for-implementation label yet. Happy
    to open it if this direction is what you want.

  11. eavanvalkenburg commented on Nov 9, 2026

    @eavanvalkenburg
    Member

    On the same Windows machine with LongPathsEnabled = 0, what do read(long_key), delete(long_key), and list_children(parent_of_long_key) return or raise? The existing plan leaves these paths unverified, and the proposed fork patch changes only write(). This result will help establish whether the fix can be limited to write-error normalization or must also change the other operations.

  12. simin-yuan commented on Nov 9, 2026

    @simin-yuan
    ContributorAuthor

    Measured on Windows 11 23H2, OS build 22631.6199 (ver reports 10.0.22631.6199;
    platform.platform() reports Windows-10-10.0.22631-SP0 because of how the OS
    version is advertised, the build number is the Windows 11 one), with
    LongPathsEnabled = 0 read back from
    HKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled (REG_DWORD 0x0)
    immediately before the run. Python 3.11.16, main at 13956ad,
    FileSystemAgentFileStore rooted at D:\m10141 (root is 9 characters, so the OS limit
    binds long before the store's own 4096/255 bound).

    key shape absolute path write() read() delete() list_children(parent)
    8 segments × 43 chars 361 FileNotFoundError, WinError 3 None False []
    9 segments × 31 chars + x.txt 303 FileNotFoundError, WinError 3 None False []
    one 300-char segment 310 FileNotFoundError, no winerror None False []
    12 segments × 20 chars 261 FileNotFoundError, no winerror None False []
    8 segments × 30 chars 257 ok "payload" True []

    Answering the question directly: read() returns None, delete() returns False,
    and list_children() on the parent returns []. None of them raise. file_exists()
    returns False as well. Listing the store root returns only the first segment that does
    exist, so the result silently stops at the first unresolvable level.

    This is not only the "the write failed, so nothing is there" case. I created the same
    tree through the \\?\ extended-length prefix so the file demonstrably exists, then
    called the store on the plain path: read() returned None, delete() returned
    False, and the file was still on disk afterwards. A file the store is responsible for
    can be neither seen nor deleted through it.

    On the error code: the failing write() above carries WinError 3
    (ERROR_PATH_NOT_FOUND), raised from Path.mkdir(parents=True, exist_ok=True) inside
    _write_file_sync. I could not produce WinError 206 for any of the five shapes on this
    host. To separate the call sites I called them directly on Python 3.11.16 and 3.14.9 —
    identical on both: os.mkdir(<300-char name>) → FileNotFoundError, WinError 3;
    os.open(<300-char name>) → FileNotFoundError with no winerror at all;
    os.makedirs(<9 × 30-char segments>/x.txt) → FileNotFoundError, WinError 206. So 206
    belongs to one recursion shape of os.makedirs, not to the call write() makes, and a
    check keyed only on winerror == 206 does not fire for anything I can reproduce: I
    applied the branch from my fork (fix/filesystem-store-enametoolong) on top of main
    and every shape in the table above still raises FileNotFoundError.

    Suggestion: gate on the resolved absolute path length rather than on one WinError.
    With long-path support disabled, any absolute path at or past the limit fails, and which
    error code surfaces depends on the call site (os.open, Path.mkdir, os.makedirs all
    differ). One check where the path is resolved — raise OSError(errno.ENAMETOOLONG) when
    sys.platform == "win32" and the absolute path is at or past the limit — would cover
    write, read, delete, list_children and file_exists in a single place, and would
    not fire on POSIX.

    Put together: this cannot be fixed in write() alone. write() at least raises;
    read(), delete(), list_children() and file_exists() turn "the path is too long"
    into "there is no such file", which is the failure mode that silently loses data and
    makes the file undeletable through the store. Whatever condition detects the over-long
    path should be applied where the path is resolved, so all four operations get it, and it
    should not depend on one specific winerror — the codes above differ per call site.

    Two things are your call:

    1. Should the read-side operations raise ENAMETOOLONG as well, or keep returning
      None/False with the limit documented? I would raise: read() returning None
      for a file that exists is the hardest failure to notice. But it is a behaviour change,
      so it is not in the patch I have on the fork.
    2. Whether the \\?\ case should be supported rather than reported — i.e. prefixing the
      absolute path when the key is over the limit instead of raising.

    Happy to extend the branch in my fork (fix/filesystem-store-enametoolong) along
    whichever line you prefer.

  13. simin-yuan commented on Nov 9, 2026

    @simin-yuan
    ContributorAuthor

    Correction to the suggestion above: the length should only rename a failure the OS has
    already raised. A check applied before the call would break hosts that have long paths
    enabled, where the same key works today. A patch following this principle is ready on my
    side; I will open a draft PR covering the write() path only, and leave the read side and
    the \\?\ question to you.

  14. added a commit that references this issue on Nov 11, 2026
    b974bc1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

harness[Issues, PRs], Target: harness-level itemsneeds-maintainer-triageUsage: [Issues], Target: all issues that the automated triage flow failed to processpythonUsage: [Issues, PRs], Target: Pythonready-for-implementationMaintainers agree the problem and direction are ready for implementation.

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions