Skip to content

Commit 52ad514

Browse files
Tighten issue 59 review follow-ups
1 parent 12d7829 commit 52ad514

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

api/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import asyncio
44
import json
5+
import logging
56
import mimetypes
67
import os
78
import tempfile
@@ -34,6 +35,7 @@
3435
DEFAULT_MEDIA_DIR = Path(os.getenv("MEMORY_MEDIA_DIR", config.MEDIA_STORAGE_PATH))
3536
_SUPPORTED_FILE_MODALITIES = {"audio", "image", "video", "multimodal"}
3637
_ALLOWED_MEDIA_TYPES = {"image", "audio", "video", "pdf"}
38+
logger = logging.getLogger(__name__)
3739

3840

3941
def _normalise_origins(origins: list[str] | None = None) -> list[str]:
@@ -344,6 +346,7 @@ def _safe_contradiction_lookup(
344346
except ValueError as exc:
345347
return ContradictionCheckResult(status="skipped", candidates=[], detail=str(exc))
346348
except Exception as exc:
349+
logger.exception("Contradiction lookup failed for semantic record %s", record.id)
347350
return ContradictionCheckResult(status="error", candidates=[], detail=str(exc))
348351

349352

@@ -418,6 +421,9 @@ def service() -> MemoryAPIService:
418421

419422
async def run_forgetting_cycle(*, dry_run: bool) -> dict[str, Any]:
420423
lock = app.state.forgetting_lock
424+
# This is intentionally single-flight per app process. Multi-worker
425+
# deployments still need an external coordinator if cross-process
426+
# forgetting exclusivity becomes a requirement.
421427
if lock.locked():
422428
return {
423429
"status": "already_running",

stores/semantic_store.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ def retrieve_by_vector(
112112
) -> list[tuple[SemanticMemory, float]]:
113113
n_results = max(1, top_k)
114114
if not include_superseded:
115+
# We need enough raw candidates to still return `top_k` active
116+
# records after filtering superseded ones, and Chroma cannot apply
117+
# that predicate natively for us. This makes default semantic recall
118+
# O(N) in collection size, which is acceptable for the current
119+
# scale but should be revisited if the store grows substantially.
115120
n_results = max(1, self._collection.count())
116121
result = self._collection.query(
117122
query_embeddings=[vector],

tests/test_forgetting_service.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,24 @@ def replace(self, record) -> None:
7373
self._records[record.id] = record
7474

7575

76+
class TrackingSemanticStore(FakeStore):
77+
def __init__(self, records=None):
78+
super().__init__(records)
79+
self.last_include_superseded = None
80+
81+
def get_all_records(
82+
self,
83+
include_embeddings: bool = False,
84+
*,
85+
include_superseded: bool = False,
86+
):
87+
self.last_include_superseded = include_superseded
88+
return super().get_all_records(
89+
include_embeddings=include_embeddings,
90+
include_superseded=include_superseded,
91+
)
92+
93+
7694
class EventRecorder:
7795
def __init__(self, bus: EventBus, *event_types: str):
7896
self.events = []
@@ -114,6 +132,31 @@ def _ids_for(report):
114132
return {decision.record_id: decision for decision in report.decisions}
115133

116134

135+
def test_scan_records_includes_superseded_semantic_rows():
136+
superseded = SemanticMemory(
137+
id="semantic-old",
138+
content="Old fact",
139+
superseded_by="semantic-new",
140+
)
141+
active = SemanticMemory(
142+
id="semantic-new",
143+
content="New fact",
144+
)
145+
semantic_store = TrackingSemanticStore([superseded, active])
146+
service = ForgettingService(
147+
semantic_store=semantic_store,
148+
episodic_store=FakeStore(),
149+
procedural_store=FakeStore(),
150+
media_store=MediaStore(tempfile.mkdtemp(prefix="forgetting_media_scan_")),
151+
contradiction_detector=_NoopDuplicateDetector(),
152+
)
153+
154+
records = service._scan_records()
155+
156+
assert semantic_store.last_include_superseded is True
157+
assert {"semantic-old", "semantic-new"} <= set(records)
158+
159+
117160
def test_dry_run_resolves_duplicate_clusters_component_wise(monkeypatch):
118161
now = datetime(2026, 3, 1, 12, 0, tzinfo=timezone.utc)
119162
first = SemanticMemory(

0 commit comments

Comments
 (0)