Repository navigation
Python: give memory topic files an injective filename stem - #9724
Manohar Paturi (ManoharPaturi) wants to merge 4 commits into
Conversation
Eduard van Valkenburg (eavanvalkenburg)
left a comment
There was a problem hiding this comment.
Manohar Paturi (@ManoharPaturi) Four material issues remain in the memory topic storage changes:
- In
python/packages/core/agent_framework/_harness/_memory.py:811-825,glob(f"{prefix}*.md")accepts longer unrelated slugs and returns the first match. A missing topic can therefore read, merge into, or delete another topic; colliding names such asC++andC#can also merge. Please validate exact topic/slug identity rather than accepting arbitrary prefixes. - In
_memory.py:884-894, updating a legacy filename writes a new digest file without retiring the matching legacy file. Both remain visible, lookup can return stale content, and deletion can resurrect the legacy record. Please implement a one-authoritative-record migration/update lifecycle. - In
_memory.py:783-784, index links and provider selection still use the old nonunique slug instead of the actual persisted filename/exact topic identity. Collision-safe files can therefore produce broken links and duplicate the wrong topic during injection. Please use the persisted storage identity consistently. - In
_memory.py:886, writing fromrecord.topicignores the publicMemoryTopicRecord(slug=...)override. Please preserve explicit slug lookup compatibility while introducing collision-safe storage identities.
Please address all four before merge.
Distinct topics can share a readable slug, so a slug-only stem let the second topic silently overwrite the first. The stem now appends a digest of the normalized topic, and the storage layer keeps one authoritative record: lookups validate every candidate against the record it actually holds instead of accepting a filename prefix, so a missing topic can never read, merge into, or delete a longer slug; rewriting a topic that lived at a legacy slug-only file retires the legacy file; index pointer links and provider lookups use the persisted stem; and an explicit slug override is persisted in the record and stays findable by that slug, without letting lookups for other names bleed into the overridden record. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
ed16818 to
8d4eb10
Compare
|
All four addressed in 8d4eb01, verified end to end against the store before pushing.
The five new tests fail on the previous head. The memory suite passes at 40 tests, ruff check and format are clean. |
|
Eduard van Valkenburg (@eavanvalkenburg) the PR body now carries the required Contribution Checklist with the non-breaking item confirmed, matching the template. Everything from your review is addressed in the latest push and the thread replies have the verification details. |
|
Manohar Paturi (@ManoharPaturi) Two additional cases at
|
The topic digest widens to 64 bits and write_topic verifies the occupant of an existing stem before reusing it, extending the digest when another topic already lives there. Slug lookups answer a single unambiguous holder again so default slug reloads work, and consolidation reloads each topic by exact identity instead of by slug. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
|
Both addressed in cef2b8e, with your two probes reproduced on the previous head first.
The four new tests fail on the previous head and pass now. The memory suite passes at 44 tests, the neighboring harness suites pass at 366, and ruff check and format are clean. One correction to the verification note above: ruff and the suites were clean, but the same push tripped Package Checks, which runs pyright over the package and rejected the store assigning the record's private stem field from outside its class. That is fixed in f1fd7fa with the stamp routed through a setter, and pyright now reports the file clean, so the earlier "clean" claim should be read as ruff only, not the full check suite. |
Package Checks flagged the store assigning the record's private on disk stem field from outside the class. The stamp now goes through a small setter so the assignment happens inside the record. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
|
The Package Checks failure was pyright flagging the store stamping the record's private on disk stem field from outside its class, three sites. Fixed in f1fd7fa: the stamp now goes through a small setter so the assignment happens inside the record. Verified with pyright against the core package config, where the file now reports clean, and the memory suite still passes at 44 tests. The merge gatekeeper was only mirroring that failure. |
| # 1. The digest-stemmed candidates for this name: the exact files | ||
| # write_topic uses, each validated against the record it holds so a | ||
| # digest collision with another topic can never answer the lookup. | ||
| for candidate in _topic_stem_candidates(topic): |
There was a problem hiding this comment.
Candidate stems are probed without first bounding their encoded filename length. For a sufficiently long readable slug, the longest candidate plus .md exceeds the filesystem component limit, so Path.exists() raises ENAMETOOLONG before lookup can reach a valid persisted or legacy filename. This can make both existing long topics and newly written topics unreadable, unwritable, or undeletable. Please bound generated candidates before touching the filesystem and resolve valid persisted/legacy stems without first probing oversized names. Add regressions for a long legacy-topic lookup and index-stem reload.
A slug long enough that its digest candidate plus the .md extension overshoots the filesystem component limit made the topic unwritable, with the errno raised before any valid persisted or legacy stem was reached. The slug part of generated candidates is capped so every candidate stays storable, oversized names are skipped rather than probed, and regressions cover a long legacy lookup and a bounded write with an index stem reload. Signed-off-by: Manohar Paturi <186662190+ManoharPaturi@users.noreply.github.com>
|
Fixed in 0a7bbf5, with the failure reproduced on the previous head first. The write path is where it bites hardest: on the previous head, writing a topic whose slug makes the primary candidate plus Regressions cover both sides: a legacy slug only file at 240 characters stays readable, and a topic with a ~470 character slug writes, reloads by topic and by its index stem, with the persisted stem asserted within the limit and every index link resolving. The write regression fails on the previous head with ENAMETOOLONG and passes now; the memory suite is at 46 tests, and pyright reports the file clean. |
|
/review |
There was a problem hiding this comment.
MAF Automated Review — Iteration 1
Result: Findings reported
Scope: full PR (4 commit(s)): 8d4eb109881a, cef2b8e95aac, f1fd7fa1b0f1, 0a7bbf53decc
Model: gpt-5.6-sol
Overview
The PR replaces non-injective slug-only filenames with bounded digest stems, validates digest occupants, preserves index links to persisted stems, and adds broad collision and compatibility coverage. Those guards address fresh same-slug collisions and overlong filenames, but the unchecked exact-file fallback can still delete an unrelated legacy topic during migration. Consolidation also regresses for the newly persisted explicit-slug records because it reloads them through the default topic-derived filename family.
Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
2 verified findings remained after source verification (1 high, 1 medium) across 1 file. Details are attached to the affected lines below.
Affected areas: python/packages/core/agent_framework/_harness/_memory.py
| else: | ||
| exact_path = topics_dir / f"{legacy_name}.md" | ||
| if exact_path.exists(): | ||
| return exact_path |
There was a problem hiding this comment.
This exact-file fallback does not verify which topic the file contains. On an upgraded store where legacy c.md holds C++, writing C# resolves that unrelated file as previous_path and unlinks it after creating the new C# file; deletion has the same cross-topic effect. Please preserve persisted-stem reads, but require destructive write/delete resolution to verify that a slug-only occupant has the requested normalized topic before it can be retired or removed.
| # Reload by the exact topic identity: a slug can be shared | ||
| # by several topics, and dropping one of them here would | ||
| # silently skip its consolidation. | ||
| current_record = self.store.get_topic(session, source_id=self.source_id, topic=record.topic) |
There was a problem hiding this comment.
An explicit-slug record such as topic Machine Learning with slug ml-notes is stored under ml-notes-<digest>.md, but reloading by record.topic searches the machine-learning-* family. Consolidation therefore catches FileNotFoundError and silently skips every such record, whereas the previous slug-based reload found it. Please reload through the known persisted stem (or otherwise resolve that exact file) while retaining identity-safe behavior for colliding default slugs.
|
Manohar Paturi (@ManoharPaturi) The new filename scheme still has two topic-identity gaps. First, |
Motivation & Context
MemoryFileStorekeyed every topic file by_slugify_topic(topic), which is many to one:C ++andC #both slug toc.write_topicoverwrites the slug path unconditionally, so writing a second topic that shares a slug silently destroyed the first topic's memories. Reproduced on main: after writing both topics,get_topic("C ++")returned the C # memory and the C ++ memory was gone. The store's own path components for owner, session, source, and scope all use the injective_storage_key_segmentderivation for exactly this reason; the topic filename was the one mapping left below that standard.Description & Review Guide
record.slug). When a slug-only lookup is ambiguous because several topics share that slug, the file whose recorded topic matches the lookup wins, else the first stem in sorted order keeps the choice deterministic._existing_topic_path, and the regression test writingC ++thenC #and reading both back, which fails on main.Related Issue
Fixes #9723
AI Assistance
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.