Repository navigation
Conversation
The description sidecar name replaced the memory file's extension, so `notes.md` and `notes` both mapped to `notes_description.md` and the two memories overwrote (and deleted) each other's description. Append the suffix to the whole memory file name instead, which keeps the mapping injective while leaving the internal-file hiding rules unchanged because every sidecar still ends in `_description.md`. Fixes microsoft#9871
There was a problem hiding this comment.
🟡 Changes recommended
Sidecar names can exceed filesystem filename limits for valid memory filenames, leaving partially written data and potentially breaking later index rebuilds.
1 open issue
What changed in this PR
This PR addresses #9871 by changing Python FileMemoryProvider sidecar names so memories such as notes.md and notes no longer share a description.
Changes:
- Append
_description.mdto the complete memory filename. - Add regression coverage for independent descriptions, rewrites, and deletion.
- Update documentation and clarify that legacy sidecars are not migrated.
| File | Description |
|---|---|
python/packages/core/tests/core/test_harness_file_memory.py |
Updates sidecar expectations and tests isolation between same-stem memories. |
python/packages/core/AGENTS.md |
Documents the naming scheme and lack of legacy migration. |
python/packages/core/agent_framework/_harness/_file_memory.py |
Preserves the full filename when deriving sidecar names and updates docstrings. |
Review effort: Balanced
| dot_index = file_name.rfind(".") | ||
| if dot_index > 0: | ||
| return f"{file_name[:dot_index]}{_DESCRIPTION_SUFFIX}" | ||
| return f"{file_name}{_DESCRIPTION_SUFFIX}" |
|
@microsoft-github-policy-service agree |
…filesystem limits Appending _description.md to a near-limit memory file name could produce a sidecar past the common 255-byte filename cap, leaving the memory body written but its description unwritable. Names that would overflow now derive a SHA-256 digest sidecar that keeps the _description.md suffix, so listings still hide it and distinct long names stay distinct.
|
Addressed the Copilot review comment about sidecar filenames exceeding filesystem name limits: Covered by |
BlueRaddish
left a comment
There was a problem hiding this comment.
The digest fallback still collides with a valid short memory filename. At head 85c87ce4974b5aa2b95c495a9185639136f32f55, take:
long_name = "a" * 238 + ".md"
short_name = "~memory-desc-" + hashlib.sha256(long_name.encode()).hexdigest()Both names pass the provider's filename validation. The long name takes the digest branch, while the short name takes the verbatim branch, and both resolve to the same ~memory-desc-ffbd1fac2db7b2978c6e59c1424fa35068080eecd1510570b0d8223907c241bc_description.md sidecar. This requires no SHA-256 collision.
I reproduced the effect through FileMemoryProvider.before_run and its actual file_memory_write, file_memory_ls, and file_memory_delete tools with InMemoryAgentFileStore: writing the short memory changes the long memory's description from long-description to short-description; deleting the short memory then removes the long memory's description. The long memory's body and an unrelated memory's description remain intact.
Please make the digest and verbatim namespaces disjoint, including valid short names beginning with ~memory-desc-, and add a cross-branch lifecycle regression. The current test compares a long name with ~memory-desc-x, which does not select the matching digest and misses this case.
The 41 existing test_harness_file_memory.py tests pass on the same exact source. My separate public-tool consumer has two failing isolation checks and two passing controls. Environment: Windows 11, Python 3.11.17, existing development dependencies; this is an in-process store/tool check, with no live model, service, or disk-backed filesystem claim. No product source was modified. Analysis and reproduction preparation were AI-assisted.
| if len(sidecar.encode("utf-8")) <= _MAX_DESCRIPTION_FILE_NAME_BYTES: | ||
| return sidecar | ||
| digest = hashlib.sha256(file_name.encode("utf-8")).hexdigest() | ||
| return f"{_DIGEST_DESCRIPTION_PREFIX}{digest}{_DESCRIPTION_SUFFIX}" |
There was a problem hiding this comment.
The digest and verbatim branches still share an output namespace. For any long filename L, a valid short memory named ~memory-desc-<sha256(L)> produces the same description sidecar, so writing, rewriting, or deleting either memory can overwrite or remove the other memory’s description without any SHA-256 collision. Please make the two namespaces disjoint—for example, also encode or hash input names that start with the reserved digest prefix—and add a regression covering a long filename and its matching digest-stem short filename.
A valid short memory name beginning with the reserved digest prefix could resolve to the same sidecar as a long name with the matching digest, so writes and deletes crossed memories without any SHA-256 collision. Names starting with ~memory-desc- now always take the digest branch, which keeps the verbatim namespace free of the reserved prefix. Add the requested cross-branch lifecycle regression (write/list/read/delete/rewrite) using a long name and its digest-stem short name, plus a disk-backed regression with FileSystemAgentFileStore. Both new lifecycle tests fail on the previous derivation; the file-memory suite passes 43 tests and ruff is clean.
|
Addressed in b3ec135. Thanks BlueRaddish for the concrete reproduction and Eduard van Valkenburg (@eavanvalkenburg) for the suggested fix. Namespaces are now disjoint by construction. Regressions added:
Verification: the file-memory suite passes 43 tests; ruff check and format are clean. The only failure in the neighboring harness suites ( |
|
Lushiyao5 The reserved digest-prefix check is case-sensitive, so uppercase or mixed-case variants can bypass it on default case-insensitive APFS and NTFS. A valid short topic named with |
On case-insensitive filesystems (default APFS and NTFS) an uppercase or mixed-case reserved prefix would bypass the startswith check and let a valid short memory alias a long memory's digest sidecar, so writing or deleting the short memory could clobber the long memory's description. Match the reserved prefix case-insensitively so case variants also take the digest branch, and add uppercase and mixed-case lifecycle regressions covering write, read, and delete.
|
Addressed in 1d77069. Thanks Eduard van Valkenburg (@eavanvalkenburg) — good catch on the case-insensitive filesystems. The reserved prefix is now matched case-insensitively. Regressions added:
Verification: the file-memory suite passes 45 tests; ruff check and format are clean. |

Motivation & Context
FileMemoryProviderderives a memory's description sidecar by replacing the file's extension: bothnotes.mdandnotesmap tonotes_description.md. Two distinct memories therefore share one sidecar, so writing a description for one silently rewrites the other's, rewriting one file without a description deletes the other's description, and deleting either file removes both.Description & Review Guide
_description_file_namenow appends_description.mdto the whole memory file name (notes.md→notes.md_description.md), which makes the derivation injective: distinct memory files always get distinct sidecars. The module docstring,AGENTS.md, and the existing tests are updated to the new name, and tests cover names that share a stem (notes.md/notes), rewriting one file without a description, and deleting one file.AGENTS.mddocuments this. Reading and rewriting a memory recreates its sidecar under the new name, and the old orphaned sidecar is simply unused.Related Issue
Fixes #9871