Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions plugins/memory/holographic/retrieval.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def search(
# Sort by score descending, return top limit
scored.sort(key=lambda x: x["score"], reverse=True)
results = scored[:limit]
self._mark_retrieved(results)
# Strip raw HRR bytes — callers expect JSON-serializable dicts
for fact in results:
fact.pop("hrr_vector", None)
Expand Down Expand Up @@ -187,7 +188,9 @@ def probe(
scored.append(fact)

scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:limit]
results = scored[:limit]
self._mark_retrieved(results)
return results

def related(
self,
Expand Down Expand Up @@ -255,7 +258,9 @@ def related(
scored.append(fact)

scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:limit]
results = scored[:limit]
self._mark_retrieved(results)
return results

def reason(
self,
Expand Down Expand Up @@ -333,7 +338,9 @@ def reason(
scored.append(fact)

scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:limit]
results = scored[:limit]
self._mark_retrieved(results)
return results

def contradict(
self,
Expand Down Expand Up @@ -476,7 +483,19 @@ def _score_facts_by_vector(
scored.append(fact)

scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:limit]
results = scored[:limit]
self._mark_retrieved(results)
return results

def _mark_retrieved(self, facts: list[dict]) -> None:
"""Record that returned facts were retrieved by a user/tool query."""
fact_ids = [fact["fact_id"] for fact in facts if "fact_id" in fact]
try:
self.store.mark_retrieved(fact_ids)
except Exception:
# Retrieval counters are advisory; never fail a memory lookup over
# usage-metric bookkeeping.
pass

def _fts_candidates(
self,
Expand Down
22 changes: 15 additions & 7 deletions plugins/memory/holographic/store.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,16 +229,24 @@ def search_facts(
results = [self._row_to_dict(r) for r in rows]

if results:
ids = [r["fact_id"] for r in results]
placeholders = ",".join("?" * len(ids))
self._conn.execute(
f"UPDATE facts SET retrieval_count = retrieval_count + 1 WHERE fact_id IN ({placeholders})",
ids,
)
self._conn.commit()
self.mark_retrieved([r["fact_id"] for r in results])

return results

def mark_retrieved(self, fact_ids: list[int]) -> None:
"""Increment retrieval_count once for each unique fact id."""
unique_ids = sorted({int(fid) for fid in fact_ids})
if not unique_ids:
return

with self._lock:
placeholders = ",".join("?" * len(unique_ids))
self._conn.execute(
f"UPDATE facts SET retrieval_count = retrieval_count + 1 WHERE fact_id IN ({placeholders})",
unique_ids,
)
self._conn.commit()

def update_fact(
self,
fact_id: int,
Expand Down
104 changes: 104 additions & 0 deletions tests/plugins/memory/test_holographic_retrieval_count.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""Regression coverage for holographic fact retrieval counters."""

from __future__ import annotations

import json

import pytest

from plugins.memory.holographic import HolographicMemoryProvider
from plugins.memory.holographic import holographic as hrr


def _make_provider(tmp_path):
provider = HolographicMemoryProvider(
config={"db_path": str(tmp_path / "memory_store.db"), "hrr_dim": 64}
)
provider.initialize(session_id="test-session")
return provider


def _call_fact_store(provider, **args):
return json.loads(provider.handle_tool_call("fact_store", args))


def _counts_by_id(provider):
return {
fact["fact_id"]: fact["retrieval_count"]
for fact in provider._store.list_facts(limit=20)
}


def test_search_increments_retrieval_count_for_returned_facts(tmp_path):
provider = _make_provider(tmp_path)
try:
kept = _call_fact_store(
provider,
action="add",
content='"Hermes" records holographic memory retrieval counts.',
category="tool",
)["fact_id"]
other = _call_fact_store(
provider,
action="add",
content='"Cafe24" deployment cache behavior is unrelated.',
category="tool",
)["fact_id"]

result = _call_fact_store(
provider,
action="search",
query="Hermes holographic retrieval",
limit=1,
)

assert [fact["fact_id"] for fact in result["results"]] == [kept]
assert _counts_by_id(provider) == {kept: 1, other: 0}

_call_fact_store(
provider,
action="search",
query="Hermes holographic retrieval",
limit=1,
)
assert _counts_by_id(provider) == {kept: 2, other: 0}
finally:
provider.shutdown()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probe case has no category, so it takes probe's direct scoring path rather than the category-bank branch that calls _score_facts_by_vector() (retrieval.py:139-152). Please add a categorized probe assertion so the new bookkeeping in that helper has regression coverage.



@pytest.mark.parametrize(
("args"),
[
{"action": "probe", "entity": "Hermes", "limit": 2},
{"action": "related", "entity": "Hermes", "limit": 2},
{"action": "reason", "entities": ["Hermes", "Memory"], "limit": 2},
],
)
def test_structural_retrieval_increments_returned_fact_counts(tmp_path, args):
if not hrr._HAS_NUMPY:
pytest.skip("structural holographic retrieval requires numpy")

provider = _make_provider(tmp_path)
try:
_call_fact_store(
provider,
action="add",
content='"Hermes" and "Memory" should update retrieval counters.',
category="tool",
)
_call_fact_store(
provider,
action="add",
content='"Hermes" and "Cron" share operational context.',
category="tool",
)

result = _call_fact_store(provider, **args)
returned_ids = {fact["fact_id"] for fact in result["results"]}

assert returned_ids
counts = _counts_by_id(provider)
for fact_id in returned_ids:
assert counts[fact_id] == 1
finally:
provider.shutdown()
Loading