Skip to content

Commit 9862ef5

Browse files
groksrcgithub-actions[bot]claudephernandez
authored
fix(core): use updated_at for recent_activity filter and ordering (#812)
Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Signed-off-by: Drew Cain <groksrc@gmail.com> Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: phernandez <paul@basicmachines.co>
1 parent df5e8d8 commit 9862ef5

3 files changed

Lines changed: 76 additions & 9 deletions

File tree

src/basic_memory/repository/postgres_search_repository.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,8 @@ async def _build_fts_query_parts(
774774
# Handle date filter
775775
if after_date:
776776
params["after_date"] = after_date
777-
conditions.append("search_index.created_at > :after_date")
777+
# Filter on updated_at so recently-edited notes are included even when created_at is old
778+
conditions.append("search_index.updated_at > :after_date")
778779
# order by most recent first
779780
order_by_clause = ", search_index.updated_at DESC"
780781

@@ -945,7 +946,7 @@ async def search(
945946
{score_expr} as score
946947
FROM {from_clause}
947948
WHERE {where_clause}
948-
ORDER BY score DESC, search_index.id ASC {order_by_clause}
949+
ORDER BY score DESC {order_by_clause}, search_index.id ASC
949950
LIMIT :limit
950951
OFFSET :offset
951952
"""

src/basic_memory/repository/sqlite_search_repository.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,8 @@ async def _build_fts_query_parts(
789789
# Handle date filter using datetime() for proper comparison
790790
if after_date:
791791
params["after_date"] = after_date
792-
conditions.append("datetime(search_index.created_at) > datetime(:after_date)")
792+
# Filter on updated_at so recently-edited notes are included even when created_at is old
793+
conditions.append("datetime(search_index.updated_at) > datetime(:after_date)")
793794

794795
# order by most recent first
795796
order_by_clause = ", search_index.updated_at DESC"

tests/services/test_search_service.py

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""Tests for search service."""
22

3-
from datetime import datetime
3+
from datetime import datetime, timezone
44

55
import pytest
66
from sqlalchemy import text
@@ -174,12 +174,12 @@ async def test_after_date(search_service, test_graph):
174174
)
175175
for r in results:
176176
# Handle both string (SQLite) and datetime (Postgres) formats
177-
created_at = (
178-
r.created_at
179-
if isinstance(r.created_at, datetime)
180-
else datetime.fromisoformat(r.created_at)
177+
updated_at = (
178+
r.updated_at
179+
if isinstance(r.updated_at, datetime)
180+
else datetime.fromisoformat(r.updated_at)
181181
)
182-
assert created_at > past_date
182+
assert updated_at > past_date
183183

184184
# Should not find with future date
185185
future_date = datetime(2030, 1, 1).astimezone()
@@ -192,6 +192,71 @@ async def test_after_date(search_service, test_graph):
192192
assert len(results) == 0
193193

194194

195+
@pytest.mark.asyncio
196+
async def test_after_date_uses_updated_at(search_service):
197+
"""Regression: after_date should filter on updated_at, not created_at.
198+
199+
An entity created before the timeframe but updated within it must appear
200+
in recent-activity results. A stale entity (updated_at also old) must not.
201+
"""
202+
cutoff = datetime(2020, 1, 1, tzinfo=timezone.utc)
203+
old_created = datetime(2015, 6, 1, tzinfo=timezone.utc)
204+
recently_updated = datetime(2023, 3, 15, tzinfo=timezone.utc)
205+
stale_updated = datetime(2018, 6, 1, tzinfo=timezone.utc)
206+
207+
project_id = search_service.repository.project_id
208+
209+
# Leave metadata at its None default — SearchIndexRow.to_insert only
210+
# JSON-serializes truthy metadata, so passing {} would slip an
211+
# un-serialized dict into the SQLite bind and raise ProgrammingError.
212+
recently_updated_row = SearchIndexRow(
213+
project_id=project_id,
214+
id=99001,
215+
type="entity",
216+
file_path="test/recently_updated.md",
217+
title="Recently Updated Entity",
218+
content_snippet="recently updated content",
219+
permalink="test/recently-updated-entity",
220+
created_at=old_created,
221+
updated_at=recently_updated,
222+
)
223+
stale_row = SearchIndexRow(
224+
project_id=project_id,
225+
id=99002,
226+
type="entity",
227+
file_path="test/stale.md",
228+
title="Stale Entity",
229+
content_snippet="stale content",
230+
permalink="test/stale-entity",
231+
created_at=old_created,
232+
updated_at=stale_updated,
233+
)
234+
235+
await search_service.repository.index_item(recently_updated_row)
236+
await search_service.repository.index_item(stale_row)
237+
238+
results = await search_service.search(
239+
SearchQuery(after_date=cutoff.isoformat())
240+
)
241+
242+
permalinks = {r.permalink for r in results}
243+
# recently-updated entity must appear despite old created_at
244+
assert "test/recently-updated-entity" in permalinks
245+
# stale entity must not appear (updated_at is before cutoff)
246+
assert "test/stale-entity" not in permalinks
247+
248+
# results should be ordered newest updated_at first
249+
updated_ats = []
250+
for r in results:
251+
ua = (
252+
r.updated_at
253+
if isinstance(r.updated_at, datetime)
254+
else datetime.fromisoformat(r.updated_at)
255+
)
256+
updated_ats.append(ua.replace(tzinfo=timezone.utc) if ua.tzinfo is None else ua)
257+
assert updated_ats == sorted(updated_ats, reverse=True)
258+
259+
195260
@pytest.mark.asyncio
196261
async def test_search_type(search_service, test_graph):
197262
"""Test search filters."""

0 commit comments

Comments
 (0)