Technical writeup on two independently discovered bugs in Memanto's memory retrieval and storage pipeline. Both bugs cause silent data loss — no errors, no warnings, just missing results.
Context
Memanto is an open-source agent memory system (1.6k+ stars) that provides persistent memory storage and retrieval for AI agents. It uses a vector store backend (Moorcheh) for similarity search, with post-retrieval filters for temporal scoping and confidence thresholding.
The system has three distinct filtering layers:
- Backend similarity search — returns top-K ranked results from the vector store
-
Post-retrieval temporal filter — narrows results to a time window specified by
created_after/created_before - Post-retrieval TTL filter — removes expired memories based on a configurable TTL
The bugs live in the interaction between these layers.
Bug 1: TTL Timeline Amnesia
Severity: HIGH
CWE-220: Sensitive Data Under Different Partition
Root Cause
The _fetch_all_memories method fetches results from the backend, then runs post-retrieval filtering. The problem: it only fetches limit + offset rows from the backend before filtering.
# Pseudocode of the vulnerable path
def search_memories(query, limit, created_after=None, created_before=None):
results = backend.similarity_search(query, top_k=limit + offset)
results = _apply_temporal_filter(results, created_after, created_before)
results = _filter_expired_memories(results)
return results[:limit]
When a temporal window is narrow (e.g., "memories from January 2026") but the top-ranked similarity results are all from a different time period (e.g., June 2026), the January memories get silently excluded — they never made it into the first limit + offset batch.
This makes search_as_of (historical memory queries) non-deterministic. Running the same query twice can return different results depending on which memories happen to rank higher in similarity.
Reproduction
- Store 20 "recent" memories (June 2026) and 5 "old" memories (January 2026)
- All recent memories rank higher than old ones by vector similarity
- Query with
created_after="2026-01-01"andcreated_before="2026-01-31"andlimit=10 - Expected: 5 January memories returned
- Actual: 0 results — all 10 fetched rows are from June, and none pass the temporal filter
Fix
The candidate pool must be widened whenever a temporal or TTL filter is active. Instead of fetching limit + offset rows, the backend query should request MOORCHEH_MAX_TOP_K (the maximum the vector store supports), then apply post-retrieval filters on the full candidate set before paginating.
# Fixed path
def search_memories(query, limit, created_after=None, created_before=None):
has_post_filter = bool(created_after or created_before)
fetch_k = MOORCHEH_MAX_TOP_K if has_post_filter else limit + offset
results = backend.similarity_search(query, top_k=fetch_k)
results = _apply_temporal_filter(results, created_after, created_before)
results = _filter_expired_memories(results)
return results[:limit]
Critical detail: This widening must also apply when only the TTL filter is active (no user-specified temporal filter), because expired top-ranked rows can crowd out valid lower-ranked memories the same way a temporal window can.
Why This Is Subtle
Most temporal queries during development use recent time windows that overlap with the recency-biased ranking, so the bug never manifests. It only surfaces when querying historical data or when the TTL window is tight relative to the data distribution.
Bug 2: Batch Upload Silent Data Loss
Severity: HIGH
CWE-252: Unchecked Return Value
Root Cause
batch_store_memories sends multiple documents to the backend in a single call but only checks the top-level response status. Individual document-level upload failures are silently swallowed.
# Pseudocode of the vulnerable path
def batch_store_memories(documents):
response = backend.documents.upload(namespace, documents)
if response["status"] == "ok":
# All good, right?
return {"stored": len(documents)}
raise StorageError("batch upload failed")
The backend's batch upload endpoint returns per-document status (documents[].status) alongside the overall response. If 3 out of 10 documents fail validation, serialization, or size checks, the caller sees status: "ok" and reports 10 stored — but only 7 actually persisted.
Reproduction
- Prepare a batch of 10 documents where 3 contain payloads that exceed the backend's per-document size limit
- Call
batch_store_memories(batch) - Response:
{"stored": 10} - Query for the 3 oversized documents — they don't exist
Fix
Validate each document's upload result individually:
def batch_store_memories(documents):
response = backend.documents.upload(namespace, documents)
individual = response.get("documents", [])
failed = [d["id"] for d in individual if d.get("status") != "ok"]
if failed:
logger.warning("batch_store: %d/%d documents failed", len(failed), len(documents))
stored = len(documents) - len(failed)
return {"stored": stored, "failed": failed if failed else None}
Why This Is Subtle
The top-level status is "ok" because the HTTP request succeeded — the backend accepted the batch. The per-document failures are buried in the response body. Developers testing with small, valid documents never hit the failure path.
Impact
Both bugs are silent — no exceptions, no log warnings, no indication that data was lost. For an AI agent memory system, data loss means:
- Lost context: The agent forgets information it was told to remember
- Non-deterministic recall: The same query returns different results at different times
-
Corrupted memory timeline: Historical queries (
search_as_of) produce wrong answers
These are exactly the failure modes that erode trust in agent memory systems. When an agent says "I remember X" and then can't recall X on the next query, users assume the AI is unreliable — when in fact the memory system is silently dropping data.
Fixes Submitted
Both fixes are included in PR [^1610](https://github.com/moorcheh-ai/memanto/pull/1610) on the Memanto repository, along with deterministic regression tests that reproduce each bug.
-
test_memory_read_temporal_recall.py— verifies that time-scoped queries recall rows outside the top similarity page -
test_backend.py— verifies batch upload per-document validation -
test_temporal_helpers.py— verifies timestamp parsing edge cases and filter robustness
Takeaways
For systems with layered filtering: Always consider the interaction between ranking and filtering. A filter that operates after ranking can silently drop results that should have been included if the candidate pool is smaller than the filter window.
For batch operations: Never trust a top-level success status. Validate per-item results. Batch APIs often return partial success, and partial success without explicit handling is just data loss with a green checkmark.

Top comments (0)