fix(memory): the link graph keys entries by path too, not by filename (#1545)
#1545 fixed the MEMORY.md index row; the semantic-link block beside it keyed candidates by path.name for the same reason and kept the same collision. Under VT_MEMORY_HIERARCHY two entries sharing a title live at "{memory_type}/{slug}.md", so discover_links() excluded the OTHER entry as "self" (it excludes by that key) and any target it wrote named a file that existed twice. The reader side accepts all three shapes a sidecar can hold: a relative path as written from now on, an absolute path as older callers wrote, and a bare filename, which can only ever be matched by stem.
This commit is contained in:
1 parent
d1605bcdc6
commit
06fa39f766
2 files changed
+60
-6
No files matched your search
@@ -438,16 +438,31 @@ class PersistentMemory:
|
||||
linker = SemanticLinker(self._dir)
|
||||
all_entries = self._scan_entries()
|
||||
linked_ids: set[str] = set()
|
||||
linked_paths: set[str] = set()
|
||||
for r in results:
|
||||
relations = linker.load_relations(r.path)
|
||||
for target_file, _score in relations:
|
||||
linked_ids.add(Path(target_file).stem)
|
||||
# A target written since the hierarchy fix is the path
|
||||
# relative to the memory dir; older sidecars hold a
|
||||
# bare filename or an absolute path, and a bare one can
|
||||
# only ever be matched by stem.
|
||||
if "/" in target_file or "\\" in target_file:
|
||||
linked_paths.add(Path(target_file).as_posix())
|
||||
else:
|
||||
linked_ids.add(Path(target_file).stem)
|
||||
# Add linked entries not already in results
|
||||
result_paths = {r.path for r in results}
|
||||
for entry in all_entries:
|
||||
if len(results) >= max_results:
|
||||
break
|
||||
if entry.path.stem in linked_ids and entry.path not in result_paths:
|
||||
if entry.path in result_paths:
|
||||
continue
|
||||
relative = entry.path.relative_to(self._dir).as_posix()
|
||||
if (
|
||||
relative in linked_paths
|
||||
or entry.path.as_posix() in linked_paths
|
||||
or entry.path.stem in linked_ids
|
||||
):
|
||||
results.append(entry)
|
||||
except Exception:
|
||||
logger.debug("semantic link expansion failed", exc_info=True)
|
||||
@@ -571,14 +586,22 @@ class PersistentMemory:
|
||||
entry_tokens = _tokenize_for_bm25(
|
||||
f"{new_entry.title} {new_entry.description} {new_entry.body}"
|
||||
)
|
||||
# Same key as the index row above, for the same
|
||||
# reason: under hierarchy mode two entries sharing a
|
||||
# title have the same path.name, so keying the link
|
||||
# graph by it made discover_links() drop the OTHER
|
||||
# entry as "self" and mint an ambiguous target.
|
||||
all_entries_data = [
|
||||
(e.path.name, _tokenize_for_bm25(
|
||||
f"{e.title} {e.description} {e.body}"
|
||||
))
|
||||
(e.path.relative_to(self._dir).as_posix(),
|
||||
_tokenize_for_bm25(
|
||||
f"{e.title} {e.description} {e.body}"
|
||||
))
|
||||
for e in all_entries if e.path != path
|
||||
]
|
||||
links = linker.discover_links(
|
||||
entry_title=new_entry.path.name,
|
||||
entry_title=new_entry.path.relative_to(
|
||||
self._dir
|
||||
).as_posix(),
|
||||
entry_tokens=entry_tokens,
|
||||
all_entries_data=all_entries_data,
|
||||
)
|
||||
|
||||
@@ -317,6 +317,37 @@ class TestAdd:
|
||||
assert "reference/kb-entry.md" in index
|
||||
assert "user/kb-entry.md" in index
|
||||
|
||||
def test_hierarchy_same_title_is_a_link_candidate_not_self(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# The link graph keyed candidates by path.name too, so under
|
||||
# VT_MEMORY_HIERARCHY the OTHER entry sharing this title was dropped
|
||||
# from the corpus as "self" (discover_links excludes by that key) and
|
||||
# any target it did write was ambiguous between the two files.
|
||||
monkeypatch.setenv("VT_MEMORY_HIERARCHY", "1")
|
||||
monkeypatch.setenv("VT_MEMORY_LINKS", "true")
|
||||
pm = PersistentMemory(memory_dir=tmp_path)
|
||||
pm.add(
|
||||
"kb-entry",
|
||||
"tencent kline pagination walks backward from end",
|
||||
"reference",
|
||||
description="tencent kline pagination reference",
|
||||
)
|
||||
second = pm.add(
|
||||
"kb-entry",
|
||||
"tencent kline pagination walks backward from end",
|
||||
"user",
|
||||
description="tencent kline pagination preference",
|
||||
)
|
||||
assert second is not None
|
||||
|
||||
from src.memory.semantic_links import SemanticLinker
|
||||
|
||||
targets = [
|
||||
target for target, _score in SemanticLinker(tmp_path).load_relations(second)
|
||||
]
|
||||
assert "reference/kb-entry.md" in targets
|
||||
|
||||
def test_recovered_orphan_and_new_write_agree_on_the_same_path(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
|
||||
Reference in new issue
Block a user