Repository navigation
Description
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:
- 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.OSErrorwitherrno.ENAMETOOLONG(which the same store already produces for the 8000-segment case) would be accurate and consistent. - 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
- On a Windows machine, confirm the setting:
reg query "HKLM\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled
# LongPathsEnabled REG_DWORD 0x0- Run the existing test:
uv run pytest tests/core/test_harness_file_access.py::test_filesystem_store_matches_in_memory_complexity_bound -q
- 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 = 1claim 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 seesFileNotFoundError. - 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.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Nov 6, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
python/packages/core/agent_framework/_harness/_file_access.py::FileSystemAgentFileStore._write_file_syncpropagatesFileNotFoundError(errno=2, winerror=206)unchanged when Windows rejects an absolute path exceeding the legacy limit. Minimal repro: inject that exception fromPath.mkdirduringFileSystemAgentFileStore.write()or run the existing complexity-bound test on Windows withLongPathsEnabled=0.python/packages/core/tests/core/test_harness_file_access.py::test_filesystem_store_normalizes_windows_path_too_long_error1.24.01.24.0westey-m commentedon Nov 6, 2026
/plan
github-actions commentedon Nov 6, 2026
Summary
On Windows with long paths turned off,
FileSystemAgentFileStore.write()fails withFileNotFoundError(errno 2,WinError 206) when the full path is longer than about 260 characters.InMemoryAgentFileStoreaccepts 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_syncin_file_access.pycallsmkdir(parents=True)and thenos.open(...). Both can raise the Windows "path too long" error. OnlyFileExistsError,PermissionErrorandELOOPare handled. Anything else propagates unchanged, so callers seeFileNotFoundError.OSError(errno.ENAMETOOLONG, ...)for paths past its internal size bound (around line 351). SoENAMETOOLONGis already the framework's way to say "path too long".read()callsfull_path.is_file()first. As far as I know, this returnsFalseon error rather than raising, so a too-long read likely returnsNone("file not found"). I did not verify this on Windows.test_filesystem_store_matches_in_memory_complexity_boundwrites 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.LongPathsEnabled = 1on 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)
_write_file_sync, wrap themkdirandos.opencalls. If the error iswinerror == 206(ERROR_FILENAME_EXCED_RANGE), raiseOSError(errno.ENAMETOOLONG, "File name too long", path). Keep the original error as the cause. Usegetattr(exc, "winerror", None)so the code stays safe on Linux and macOS.read()(anddelete,listand similar) should raiseENAMETOOLONGor keep returningNone. Raising matches the in-memory store's behavior for over-bound paths. Check each operation, not justwrite. I suggest the same normalization wherever a path is opened.FileSystemAgentFileStoredocstring. 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.ENAMETOOLONGthere.Path.mkdir/os.opento raiseFileNotFoundErrorwithwinerror=206, and assert the store raisesOSErrorwithENAMETOOLONG. This covers the bug on any CI host, since CI has long paths enabled.LongPathsEnabled = 0. The reporter's 200-segment probe now printsENAMETOOLONG.Assumption: the
winerrorvalue is only set on Windows. I have not tested any of this on Windows.simin-yuan commentedon Nov 6, 2026
Patch and tests are ready on my fork. Windows 11 with
LongPathsEnabled = 0reproducesthe failure; after the change the same 401-character key raises
OSError(ENAMETOOLONG)instead of
FileNotFoundError(winerror 206). It normalizes WinError 206 at the mkdir andopen call sites in
_write_file_sync, documents the limit on the store, and makes thewithin-bound half of
test_filesystem_store_matches_in_memory_complexity_boundexpectENAMETOOLONGon 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-implementationlabel yet. Happyto open it if this direction is what you want.
eavanvalkenburg commentedon Nov 9, 2026
On the same Windows machine with
LongPathsEnabled = 0, what doread(long_key),delete(long_key), andlist_children(parent_of_long_key)return or raise? The existing plan leaves these paths unverified, and the proposed fork patch changes onlywrite(). This result will help establish whether the fix can be limited to write-error normalization or must also change the other operations.simin-yuan commentedon Nov 9, 2026
Measured on Windows 11 23H2, OS build 22631.6199 (
verreports10.0.22631.6199;platform.platform()reportsWindows-10-10.0.22631-SP0because of how the OSversion is advertised, the build number is the Windows 11 one), with
LongPathsEnabled = 0read back fromHKLM\SYSTEM\CurrentControlSet\Control\FileSystem\LongPathsEnabled(REG_DWORD0x0)immediately before the run. Python 3.11.16,
mainat 13956ad,FileSystemAgentFileStorerooted atD:\m10141(root is 9 characters, so the OS limitbinds long before the store's own 4096/255 bound).
write()read()delete()list_children(parent)NoneFalse[]x.txtNoneFalse[]winerrorNoneFalse[]winerrorNoneFalse[]"payload"True[]Answering the question directly:
read()returnsNone,delete()returnsFalse,and
list_children()on the parent returns[]. None of them raise.file_exists()returns
Falseas well. Listing the store root returns only the first segment that doesexist, 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, thencalled the store on the plain path:
read()returnedNone,delete()returnedFalse, and the file was still on disk afterwards. A file the store is responsible forcan be neither seen nor deleted through it.
On the error code: the failing
write()above carries WinError 3(
ERROR_PATH_NOT_FOUND), raised fromPath.mkdir(parents=True, exist_ok=True)inside_write_file_sync. I could not produce WinError 206 for any of the five shapes on thishost. 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 nowinerrorat all;os.makedirs(<9 × 30-char segments>/x.txt)→ FileNotFoundError, WinError 206. So 206belongs to one recursion shape of
os.makedirs, not to the callwrite()makes, and acheck keyed only on
winerror == 206does not fire for anything I can reproduce: Iapplied the branch from my fork (
fix/filesystem-store-enametoolong) on top ofmainand 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.makedirsalldiffer). One check where the path is resolved — raise
OSError(errno.ENAMETOOLONG)whensys.platform == "win32"and the absolute path is at or past the limit — would coverwrite,read,delete,list_childrenandfile_existsin a single place, and wouldnot fire on POSIX.
Put together: this cannot be fixed in
write()alone.write()at least raises;read(),delete(),list_children()andfile_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:
ENAMETOOLONGas well, or keep returningNone/Falsewith the limit documented? I would raise:read()returningNonefor 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.
\\?\case should be supported rather than reported — i.e. prefixing theabsolute path when the key is over the limit instead of raising.
Happy to extend the branch in my fork (
fix/filesystem-store-enametoolong) alongwhichever line you prefer.
simin-yuan commentedon Nov 9, 2026
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 andthe
\\?\question to you.Python: report an over-long Windows path as ENAMETOOLONG, not a missi…