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
12 changes: 7 additions & 5 deletions packages/nooa-memory/src/nooa_memory/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,14 +403,16 @@ def update(
status: str | None = None,
references: list[str] | None = None,
) -> bool:
"""Refine an existing memory in place (re-embeds if content changed)."""
"""Refine a memory, refreshing derived metadata and changed embedding input."""
m = self._find(memory_id)
if m is None:
return False
self._assert_writable(m)
content_changed = content is not None and content != m.content
if content is not None:
m.content = content
embedding_before = m.embedding_text()
if content is not None and content != m.content:
# Reuse the schema's derivation rules without losing record history.
data = m.model_dump(exclude={"size_chars", "token_len", "sentence_count"})
m = Memory.model_validate({**data, "content": content})
if importance is not None:
m.importance = max(0.0, min(10.0, importance))
if type is not None:
Expand All @@ -434,7 +436,7 @@ def update(
reinforce=False,
cap=self.config.observability.access_log_cap,
)
if content_changed:
if m.embedding_text() != embedding_before:
self.store.add(m, self.embedder.embed(m.embedding_text())) # re-embed + replace
else:
self.store.save(m)
Expand Down
44 changes: 44 additions & 0 deletions packages/nooa-memory/tests/memory/test_memory_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@
"""

from pathlib import Path
from unittest.mock import patch

import numpy as np
import pytest
from nooa_memory import (
Memory,
MemoryConfig,
MemoryManager,
MemoryToolsMixin,
Expand Down Expand Up @@ -146,6 +149,7 @@ def test_remember_and_recall_via_tools(agent):


def test_remember_dedups_on_write(agent):
"""Remembering an existing fact reinforces its record instead of adding a duplicate."""
mgr = _install(agent)
id1 = agent.remember("identical fact about shipping releases", type="info")
id2 = agent.remember("identical fact about shipping releases", type="info")
Expand All @@ -154,6 +158,46 @@ def test_remember_dedups_on_write(agent):
assert mgr.store.get(id1).reinforcement_count >= 1


@pytest.mark.parametrize("content", ["A longer sentence. Another sentence! A third?", "Hi.", ""])
def test_update_refreshes_content_metadata(agent, content):
"""Content edits recalculate size metadata while preserving identity and history."""
mgr = _install(agent)
try:
mid = mgr.remember("Original content to be revised.", tags=["original"])
before = mgr.store.get(mid)
assert mgr.update(mid, content=content)
after = mgr.store.get(mid)
expected = Memory(content=content)
assert after.content == content
for field in ("size_chars", "token_len", "sentence_count"):
assert getattr(after, field) == getattr(expected, field)
for field in ("id", "owner", "created_at", "tags"):
assert getattr(after, field) == getattr(before, field)
assert after.access_log[:-1] == before.access_log
assert after.reinforcement_count == before.reinforcement_count + 1
finally:
mgr.uninstall()


@pytest.mark.parametrize("tags", [["aardvark", "zebra"], []])
def test_update_reembeds_changed_tags_only_when_needed(agent, tags):
"""Changed tags refresh the embedding, but unchanged embedding input is reused."""
mgr = _install(agent)
try:
mid = mgr.remember("A stored fact.", tags=["original"])
original = mgr.store.get_embedding(mid)
assert mgr.update(mid, tags=tags)
current = mgr.store.get(mid)
expected = mgr.embedder.embed(current.embedding_text())
assert not np.allclose(original, expected)
np.testing.assert_allclose(mgr.store.get_embedding(mid), expected)
with patch.object(mgr.embedder, "embed", wraps=mgr.embedder.embed) as embed:
assert mgr.update(mid, tags=tags, importance=9)
embed.assert_not_called()
finally:
mgr.uninstall()


# --------------------------------------------------------------------------
# spontaneous association (pre-turn injection)
# --------------------------------------------------------------------------
Expand Down