fix: add timestamps for DELETE operations in history (#4492)

This commit is contained in:
VictorECDSA
2026-03-26 18:52:10 +05:30
committed by GitHub
parent 7e06aeeada
commit 1aecfadf45
2 changed files with 158 additions and 4 deletions
+13
View File
@@ -1320,12 +1320,19 @@ class Memory(MemoryBase):
if existing_memory is None: if existing_memory is None:
raise ValueError(f"Memory with id {memory_id} not found") raise ValueError(f"Memory with id {memory_id} not found")
prev_value = existing_memory.payload.get("data", "") prev_value = existing_memory.payload.get("data", "")
# Preserve original created_at and record deletion time
created_at = _normalize_iso_timestamp_to_utc(existing_memory.payload.get("created_at"))
updated_at = datetime.now(timezone.utc).isoformat()
self.vector_store.delete(vector_id=memory_id) self.vector_store.delete(vector_id=memory_id)
self.db.add_history( self.db.add_history(
memory_id, memory_id,
prev_value, prev_value,
None, None,
"DELETE", "DELETE",
created_at=created_at,
updated_at=updated_at,
actor_id=existing_memory.payload.get("actor_id"), actor_id=existing_memory.payload.get("actor_id"),
role=existing_memory.payload.get("role"), role=existing_memory.payload.get("role"),
is_deleted=1, is_deleted=1,
@@ -2460,6 +2467,10 @@ class AsyncMemory(MemoryBase):
raise ValueError(f"Memory with id {memory_id} not found") raise ValueError(f"Memory with id {memory_id} not found")
prev_value = existing_memory.payload.get("data", "") prev_value = existing_memory.payload.get("data", "")
# Preserve original created_at and record deletion time
created_at = _normalize_iso_timestamp_to_utc(existing_memory.payload.get("created_at"))
updated_at = datetime.now(timezone.utc).isoformat()
await asyncio.to_thread(self.vector_store.delete, vector_id=memory_id) await asyncio.to_thread(self.vector_store.delete, vector_id=memory_id)
await asyncio.to_thread( await asyncio.to_thread(
self.db.add_history, self.db.add_history,
@@ -2467,6 +2478,8 @@ class AsyncMemory(MemoryBase):
prev_value, prev_value,
None, None,
"DELETE", "DELETE",
created_at=created_at,
updated_at=updated_at,
actor_id=existing_memory.payload.get("actor_id"), actor_id=existing_memory.payload.get("actor_id"),
role=existing_memory.payload.get("role"), role=existing_memory.payload.get("role"),
is_deleted=1, is_deleted=1,
+141
View File
@@ -1,4 +1,5 @@
import json import json
from datetime import datetime
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import pytest import pytest
@@ -572,3 +573,143 @@ def test_update_infer_true_caches_embedding_on_llm_rewrite(mock_sqlite, mock_llm
# It should NOT be called a 3rd time inside _update_memory # It should NOT be called a 3rd time inside _update_memory
assert embedder.embed.call_count == 2 assert embedder.embed.call_count == 2
mock_vector_store.update.assert_called_once() mock_vector_store.update.assert_called_once()
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
def test_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that deleting a memory records created_at and updated_at in history.
Ensures DELETE operations preserve the original creation timestamp
and record the deletion time for proper audit trails.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"created_at": "2024-01-01T00:00:00+00:00",
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00"
assert call_kwargs["updated_at"] is not None
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
def test_delete_memory_normalizes_non_utc_created_at(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Test that non-UTC created_at timestamps are normalized to UTC on delete."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"created_at": "2024-01-01T05:00:00+05:00", # UTC+5
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00" # normalized to UTC
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
def test_delete_memory_missing_created_at(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""Test that delete works when created_at is absent from the payload (pre-existing memories)."""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import Memory as MemoryClass
config = MemoryConfig()
memory = MemoryClass(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] is None
assert call_kwargs["updated_at"] is not None
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp
@pytest.mark.asyncio
@patch('mem0.utils.factory.EmbedderFactory.create')
@patch('mem0.utils.factory.VectorStoreFactory.create')
@patch('mem0.utils.factory.LlmFactory.create')
@patch('mem0.memory.main.SQLiteManager')
async def test_async_delete_memory_history_has_timestamps(mock_sqlite, mock_llm_factory, mock_vector_factory, mock_embedder_factory):
"""
Test that async deleting a memory records created_at and updated_at in history.
Ensures async DELETE operations preserve the original creation timestamp
and record the deletion time for proper audit trails.
"""
mock_embedder_factory.return_value = MagicMock()
mock_vector_store = MagicMock()
mock_vector_factory.return_value = mock_vector_store
mock_llm_factory.return_value = MagicMock()
mock_sqlite.return_value = MagicMock()
from mem0.memory.main import AsyncMemory
config = MemoryConfig()
memory = AsyncMemory(config)
existing_memory = MagicMock()
existing_memory.payload = {
"data": "I like Python.",
"created_at": "2024-01-01T00:00:00+00:00",
"actor_id": None,
"role": None,
}
mock_vector_store.get.return_value = existing_memory
await memory.delete("mem-123")
call_kwargs = memory.db.add_history.call_args.kwargs
assert call_kwargs["created_at"] == "2024-01-01T00:00:00+00:00"
assert call_kwargs["updated_at"] is not None
datetime.fromisoformat(call_kwargs["updated_at"]) # verify valid ISO timestamp