fix(memory): set updated_at on creation and preserve pre-existing created_at (#4499)

Co-authored-by: utkarsh240799 <utkarsh240799@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Utkarsh
2026-03-27 16:59:44 +05:30
committed by GitHub
parent e8004b93db
commit 12624555b4
2 changed files with 155 additions and 2 deletions
+8 -2
View File
@@ -1200,7 +1200,9 @@ class Memory(MemoryBase):
new_metadata = deepcopy(metadata) if metadata is not None else {}
new_metadata["data"] = data
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
if "created_at" not in new_metadata:
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
new_metadata["updated_at"] = new_metadata["created_at"]
self.vector_store.insert(
vectors=[embeddings],
@@ -1213,6 +1215,7 @@ class Memory(MemoryBase):
data,
"ADD",
created_at=new_metadata.get("created_at"),
updated_at=new_metadata.get("updated_at"),
actor_id=new_metadata.get("actor_id"),
role=new_metadata.get("role"),
)
@@ -2329,7 +2332,9 @@ class AsyncMemory(MemoryBase):
new_metadata = deepcopy(metadata) if metadata is not None else {}
new_metadata["data"] = data
new_metadata["hash"] = hashlib.md5(data.encode()).hexdigest()
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
if "created_at" not in new_metadata:
new_metadata["created_at"] = datetime.now(timezone.utc).isoformat()
new_metadata["updated_at"] = new_metadata["created_at"]
await asyncio.to_thread(
self.vector_store.insert,
@@ -2345,6 +2350,7 @@ class AsyncMemory(MemoryBase):
data,
"ADD",
created_at=new_metadata.get("created_at"),
updated_at=new_metadata.get("updated_at"),
actor_id=new_metadata.get("actor_id"),
role=new_metadata.get("role"),
)
+147
View File
@@ -208,6 +208,28 @@ def test_create_memory_uses_utc_timestamps(mocker):
_assert_utc_timestamp(payload["created_at"])
def test_create_memory_sets_updated_at(mocker):
memory = _build_memory_instance(mocker, Memory)
memory._create_memory("new memory", {"new memory": [0.1, 0.2, 0.3]}, metadata={})
payload = memory.vector_store.insert.call_args.kwargs["payloads"][0]
assert "updated_at" in payload
assert payload["updated_at"] == payload["created_at"]
_assert_utc_timestamp(payload["updated_at"])
# History should also receive updated_at
history_kwargs = memory.db.add_history.call_args
assert history_kwargs.kwargs["updated_at"] == payload["updated_at"]
def test_create_memory_preserves_existing_created_at(mocker):
memory = _build_memory_instance(mocker, Memory)
custom_ts = "2023-05-06T09:19:20+00:00"
memory._create_memory("new memory", {"new memory": [0.1, 0.2, 0.3]}, metadata={"created_at": custom_ts})
payload = memory.vector_store.insert.call_args.kwargs["payloads"][0]
assert payload["created_at"] == custom_ts
assert payload["updated_at"] == custom_ts
def test_update_memory_uses_utc_timestamps(mocker):
memory = _build_memory_instance(mocker, Memory)
memory.vector_store.get.return_value = MagicMock(
@@ -227,6 +249,30 @@ async def test_async_create_memory_uses_utc_timestamps(mocker):
_assert_utc_timestamp(payload["created_at"])
@pytest.mark.asyncio
async def test_async_create_memory_sets_updated_at(mocker):
memory = _build_memory_instance(mocker, AsyncMemory)
await memory._create_memory("new memory", {"new memory": [0.1, 0.2, 0.3]}, metadata={})
payload = memory.vector_store.insert.call_args.kwargs["payloads"][0]
assert "updated_at" in payload
assert payload["updated_at"] == payload["created_at"]
_assert_utc_timestamp(payload["updated_at"])
# History should also receive updated_at
history_kwargs = memory.db.add_history.call_args
assert history_kwargs.kwargs["updated_at"] == payload["updated_at"]
@pytest.mark.asyncio
async def test_async_create_memory_preserves_existing_created_at(mocker):
memory = _build_memory_instance(mocker, AsyncMemory)
custom_ts = "2023-05-06T09:19:20+00:00"
await memory._create_memory("new memory", {"new memory": [0.1, 0.2, 0.3]}, metadata={"created_at": custom_ts})
payload = memory.vector_store.insert.call_args.kwargs["payloads"][0]
assert payload["created_at"] == custom_ts
assert payload["updated_at"] == custom_ts
@pytest.mark.asyncio
async def test_async_update_memory_uses_utc_timestamps(mocker):
memory = _build_memory_instance(mocker, AsyncMemory)
@@ -239,6 +285,107 @@ async def test_async_update_memory_uses_utc_timestamps(mocker):
_assert_utc_timestamp(payload["updated_at"])
def test_create_then_search_and_get_all_return_same_timestamps(mocker):
"""Reproduces issue #3720: created_at must be identical in search() and get_all()."""
memory = _build_memory_instance(mocker, Memory)
# Step 1: Create a memory — capture the payload stored in the vector store
memory._create_memory("Likes pizza", {"Likes pizza": [0.1, 0.2, 0.3]}, metadata={"user_id": "alice"})
stored_payload = memory.vector_store.insert.call_args.kwargs["payloads"][0]
stored_id = memory.vector_store.insert.call_args.kwargs["ids"][0]
# Verify both timestamps were stored
assert stored_payload["created_at"] is not None
assert stored_payload["updated_at"] is not None
assert stored_payload["updated_at"] == stored_payload["created_at"]
# Step 2: Simulate the vector store returning this memory for both search and get_all
mem_result = MagicMock()
mem_result.id = stored_id
mem_result.payload = stored_payload
mem_result.score = 0.95
memory.vector_store.search.return_value = [mem_result]
memory.vector_store.list.return_value = [[mem_result]]
# Step 3: Call search and get_all, compare timestamps
search_results = memory._search_vector_store("pizza", filters={"user_id": "alice"}, limit=10, threshold=None)
get_all_results = memory._get_all_from_vector_store(filters={"user_id": "alice"}, limit=100)
search_item = search_results[0]
get_all_item = get_all_results[0]
# The core assertion from issue #3720: created_at must be the same
assert search_item["created_at"] == get_all_item["created_at"], (
f"created_at mismatch: search={search_item['created_at']}, get_all={get_all_item['created_at']}"
)
assert search_item["updated_at"] == get_all_item["updated_at"], (
f"updated_at mismatch: search={search_item['updated_at']}, get_all={get_all_item['updated_at']}"
)
# Neither should be None
assert search_item["created_at"] is not None
assert search_item["updated_at"] is not None
assert get_all_item["created_at"] is not None
assert get_all_item["updated_at"] is not None
def test_update_preserves_created_at_and_updates_updated_at(mocker):
"""After an update, created_at must stay the same and updated_at must change."""
memory = _build_memory_instance(mocker, Memory)
# Create a memory
memory._create_memory("Likes pizza", {"Likes pizza": [0.1, 0.2, 0.3]}, metadata={"user_id": "alice"})
created_payload = memory.vector_store.insert.call_args.kwargs["payloads"][0]
created_id = memory.vector_store.insert.call_args.kwargs["ids"][0]
original_created_at = created_payload["created_at"]
# Update the memory — simulate existing memory in vector store
memory.vector_store.get.return_value = MagicMock(
id=created_id,
payload=created_payload,
)
memory._update_memory(created_id, "Loves pizza", {"Loves pizza": [0.2, 0.3, 0.4]}, metadata={})
updated_payload = memory.vector_store.update.call_args.kwargs["payload"]
# created_at must be preserved
assert updated_payload["created_at"] == original_created_at
# updated_at must be set and different from creation time (or at least present)
assert updated_payload["updated_at"] is not None
_assert_utc_timestamp(updated_payload["updated_at"])
def test_search_and_get_all_consistent_after_update(mocker):
"""After update, search and get_all must still return the same timestamps."""
memory = _build_memory_instance(mocker, Memory)
# Simulate a memory that was created then updated
updated_payload = {
"data": "Loves pizza",
"hash": "abc123",
"user_id": "alice",
"created_at": "2023-05-06T09:19:20+00:00",
"updated_at": "2026-03-23T10:00:00+00:00",
}
mem_result = MagicMock()
mem_result.id = "mem-1"
mem_result.payload = updated_payload
mem_result.score = 0.9
memory.vector_store.search.return_value = [mem_result]
memory.vector_store.list.return_value = [[mem_result]]
search_results = memory._search_vector_store("pizza", filters={"user_id": "alice"}, limit=10, threshold=None)
get_all_results = memory._get_all_from_vector_store(filters={"user_id": "alice"}, limit=100)
assert search_results[0]["created_at"] == get_all_results[0]["created_at"]
assert search_results[0]["updated_at"] == get_all_results[0]["updated_at"]
# created_at should be the original, not the updated time
assert search_results[0]["created_at"] == "2023-05-06T09:19:20+00:00"
assert search_results[0]["updated_at"] == "2026-03-23T10:00:00+00:00"
class TestMetadataNotMutated:
"""Tests that metadata dicts passed to memory methods are not mutated in-place (issue #2648)."""