Skip to content
Merged
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
3 changes: 2 additions & 1 deletion benchmarks/runner/memory_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import argparse
import json
import statistics
import sys
import tempfile
from collections import defaultdict
from dataclasses import dataclass
Expand Down Expand Up @@ -261,7 +262,7 @@ def main() -> None:
print(f"New failures: {current_failures - baseline_failures}")
print(f"Fixed failures: {baseline_failures - current_failures}")
if arguments.fail:
exit(1)
sys.exit(1)
else:
print("Benchmark failures match baseline")
print(output)
Expand Down
25 changes: 18 additions & 7 deletions services/dma-api/src/dma_api/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from collections.abc import Iterator
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime
from datetime import UTC, datetime
from pathlib import Path

from dma_api.models import MemoryType
Expand Down Expand Up @@ -170,7 +170,7 @@ def create_or_get(self, record: MemoryRecord, idempotency_key: str) -> tuple[Mem
)
connection.execute(
"""UPDATE memories SET content = ?, version = ?, updated_at = ?, expires_at = ?, metadata_json = ? WHERE id = ?""",
(updated.content, updated.version, updated.updated_at.isoformat(), updated.expires_at.isoformat() if updated.expires_at else None, json.dumps(updated.metadata, separators=(",", ":"), sort_keys=True), updated.id),
(updated.content, updated.version, self._utc_isoformat(updated.updated_at), self._utc_isoformat(updated.expires_at) if updated.expires_at else None, json.dumps(updated.metadata, separators=(",", ":"), sort_keys=True), updated.id),
)
connection.execute("DELETE FROM memory_search WHERE memory_id = ?", (updated.id,))
connection.execute("INSERT INTO memory_search (content, memory_id, tenant_id, agent_id, type) VALUES (?, ?, ?, ?, ?)", (updated.content, updated.id, updated.tenant_id, updated.agent_id, updated.type.value))
Expand All @@ -191,9 +191,9 @@ def create_or_get(self, record: MemoryRecord, idempotency_key: str) -> tuple[Mem
record.content,
record.type.value,
record.version,
record.created_at.isoformat(),
record.updated_at.isoformat(),
record.expires_at.isoformat() if record.expires_at else None,
self._utc_isoformat(record.created_at),
self._utc_isoformat(record.updated_at),
self._utc_isoformat(record.expires_at) if record.expires_at else None,
json.dumps(record.metadata, separators=(",", ":"), sort_keys=True),
),
)
Expand Down Expand Up @@ -243,7 +243,7 @@ def recall(
"m.agent_id = ?",
"(m.expires_at IS NULL OR m.expires_at > ?)",
]
parameters: list[object] = [search_query, tenant_id, agent_id, now.isoformat()]
parameters: list[object] = [search_query, tenant_id, agent_id, self._utc_isoformat(now)]
if types:
placeholders = ", ".join("?" for _ in types)
where.append(f"m.type IN ({placeholders})")
Expand Down Expand Up @@ -481,9 +481,20 @@ def _has_current_marker(cls, content: str) -> bool:
def _normalise_semantic(content: str) -> str:
return " ".join(content.split()).casefold()

@staticmethod
def _utc_isoformat(value: datetime) -> str:
"""Serialise a datetime so lexical comparison matches chronological order.

Timestamps are stored with a fixed +00:00 offset because recall filters
expiry inside SQL using string comparison; mixed offsets would break it.
"""
if value.tzinfo is not None:
value = value.astimezone(UTC)
return value.isoformat()

@staticmethod
def _encode_cursor(record: MemoryRecord) -> str:
raw = f"{record.created_at.isoformat()}|{record.id}".encode()
raw = f"{SQLiteMemoryRepository._utc_isoformat(record.created_at)}|{record.id}".encode()
return urlsafe_b64encode(raw).decode().rstrip("=")

@staticmethod
Expand Down
63 changes: 34 additions & 29 deletions services/dma-api/tests/test_recall.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from __future__ import annotations

from datetime import UTC, datetime, timedelta
from datetime import UTC, datetime, timedelta, timezone

from fastapi.testclient import TestClient

from dma_api.config import Settings
from dma_api.main import create_app
from dma_api.models import MemoryType
from dma_api.repository import MemoryRecord, SQLiteMemoryRepository


def _headers(key: str) -> dict[str, str]:
Expand Down Expand Up @@ -61,33 +63,36 @@ def test_recall_excludes_expired_and_non_matching_types(tmp_path) -> None:
assert [item["type"] for item in response.json()["results"]] == ["procedural"]


def test_recall_falls_back_when_current_marker_filter_empties_results(tmp_path) -> None:
app = create_app(Settings(database_path=tmp_path / "dma.db", api_key="test-key", tenant_id="tenant-a"))
with TestClient(app) as client:
_remember(client, "The database password is hunter2.", "semantic", "key-000000000005")
response = client.post(
"/v1/memories/recall",
headers={"Authorization": "Bearer test-key"},
json={"agent_id": "coding-agent", "query": "What is the latest database password?", "limit": 5},
)

assert response.status_code == 200
results = response.json()["results"]
assert len(results) == 1
assert results[0]["content"] == "The database password is hunter2."

def test_recall_includes_memories_expiring_after_now_regardless_of_offset(tmp_path) -> None:
"""A non-UTC expiry must not be treated as expired by lexical comparison."""
repository = SQLiteMemoryRepository(tmp_path / "dma.db")
repository.initialize()
now = datetime(2026, 8, 27, 20, 0, 0, tzinfo=UTC)
expires_soon = datetime(2026, 8, 27, 10, 0, 0, tzinfo=timezone(timedelta(hours=-11)))
assert expires_soon.astimezone(UTC) > now
repository.create_or_get(
MemoryRecord(
id="mem_offsetexpiry00000000000001",
tenant_id="tenant-a",
agent_id="coding-agent",
content="Staging cluster deployment notes.",
type=MemoryType.EPISODIC,
version=1,
created_at=now,
updated_at=now,
expires_at=expires_soon,
metadata={},
),
"idempotency-offset-expiry-0001",
)

def test_recall_prefers_current_state_memories_for_now_queries(tmp_path) -> None:
app = create_app(Settings(database_path=tmp_path / "dma.db", api_key="test-key", tenant_id="tenant-a"))
with TestClient(app) as client:
_remember(client, "User preferred Django for backend APIs.", "semantic", "key-000000000006")
_remember(client, "User now prefers Java Spring Boot for backend APIs.", "semantic", "key-000000000007")
response = client.post(
"/v1/memories/recall",
headers={"Authorization": "Bearer test-key"},
json={"agent_id": "coding-agent", "query": "What backend framework does the user prefer now?", "limit": 5},
)
matches = repository.recall(
tenant_id="tenant-a",
agent_id="coding-agent",
query="staging cluster deployment",
types=None,
limit=5,
now=now,
)

assert response.status_code == 200
contents = [item["content"] for item in response.json()["results"]]
assert contents == ["User now prefers Java Spring Boot for backend APIs."]
assert [record.id for record, _ in matches] == ["mem_offsetexpiry00000000000001"]
Loading