You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In _process_metadata_filters, an AND condition whose key duplicates a sibling top-level key silently replaces that key instead of being ANDed with it. The surviving value depends on dict insertion order, so the same logical filter returns different results depending on how the caller happened to build the dict.
merge_filters deep-merges when both values are dicts, but falls through to a plain assignment for scalars (mem0/memory/main.py:1579, and the AsyncMemory twin at :3267):
This is the unfinished half of #4853, which fixed the operator-dict collision ({"price": {"gt": 10}} + {"price": {"lt": 20}}) and introduced this helper. The scalar collision was left overwriting.
Steps to Reproduce
No API keys needed — drives Memory against an in-memory Qdrant with a stub embedder:
{"user_id": "alice", "AND": [{"user_id": "bob"}]} means user_id == aliceanduser_id == bob, which nothing can satisfy, so it should return [].
docs/open-source/features/metadata-filtering.mdx states this directly:
Sibling top-level keys are implicitly ANDed on both self-hosted and the hosted Platform API, so a flat filter like {"user_id": "alice", "category": "work"} works without wrapping it in AND on either.
And the operator table lists AND as "Combine filters".
Actual Behavior
The top-level user_id is dropped and only the AND condition survives — and swapping the insertion order of the two keys flips the answer:
user_id=alice -> ['alice']
alice AND user_id=bob -> ['bob'] # expected []
same filter, keys inserted swapped -> ['alice'] # expected []
alice OR user_id=bob -> [] # correct
alice AND category=work (no clash) -> ['alice'] # correct
Environment
mem0 version: 2.2.1; also present on main at abb81c88
Python version: 3.12
Vector store: Qdrant (in-memory); the defect is in mem0/memory/main.py, before any store-specific translation
OS: Linux
How You Verified This
What I Ran
The script above, against a clean checkout, with no LLM and no embedding API key.
What I Saw
The five lines quoted under Actual Behavior.
Why This Is a Bug
Three independent signals, not just the doc wording:
It is order-dependent. The same logical filter returns ['bob'] or ['alice'] purely based on which key was inserted first. Nothing in the documented grammar makes filter results depend on dict construction order.
OR gets the identical case right.{"user_id": "alice", "OR": [{"user_id": "bob"}]} correctly returns [], because OR is written to $or as a separate key and therefore still ANDs with the sibling. Only AND merges into the same dict and clobbers. That inconsistency inside one function is hard to read as intentional.
Not a security issue, and I am deliberately not reporting it as one.search() takes a single caller-built filters dict with no separately trusted scope parameter. The one first-party merge point, server/main.py:455-461, assigns filters["user_id"]after any client-supplied AND, so the server's value wins and the scope holds — I tested that path specifically and it returned ['alice']. Exploiting this would require a downstream application to merge untrusted input into its own scope dict and order the keys unfavorably, which is an application bug rather than a trust-boundary break in mem0.
Suggested Fix
Make the scalar branch combine rather than replace, so a genuine contradiction yields no results instead of silently picking a winner — for example by folding a colliding scalar into an eq operator dict alongside the existing value, or by rejecting contradictory equality constraints outright. Both Memory and AsyncMemory copies would need it.
AI Assistance
AI helped me find it, and I reproduced it myself afterwards
Component
Python SDK
Description
Summary
In
_process_metadata_filters, anANDcondition whose key duplicates a sibling top-level key silently replaces that key instead of being ANDed with it. The surviving value depends on dict insertion order, so the same logical filter returns different results depending on how the caller happened to build the dict.merge_filtersdeep-merges when both values are dicts, but falls through to a plain assignment for scalars (mem0/memory/main.py:1579, and theAsyncMemorytwin at:3267):This is the unfinished half of #4853, which fixed the operator-dict collision (
{"price": {"gt": 10}}+{"price": {"lt": 20}}) and introduced this helper. The scalar collision was left overwriting.Steps to Reproduce
No API keys needed — drives
Memoryagainst an in-memory Qdrant with a stub embedder:Expected Behavior
{"user_id": "alice", "AND": [{"user_id": "bob"}]}meansuser_id == aliceanduser_id == bob, which nothing can satisfy, so it should return[].docs/open-source/features/metadata-filtering.mdxstates this directly:And the operator table lists
ANDas "Combine filters".Actual Behavior
The top-level
user_idis dropped and only theANDcondition survives — and swapping the insertion order of the two keys flips the answer:Environment
2.2.1; also present onmainatabb81c88mem0/memory/main.py, before any store-specific translationHow You Verified This
What I Ran
The script above, against a clean checkout, with no LLM and no embedding API key.
What I Saw
The five lines quoted under Actual Behavior.
Why This Is a Bug
Three independent signals, not just the doc wording:
['bob']or['alice']purely based on which key was inserted first. Nothing in the documented grammar makes filter results depend on dict construction order.ORgets the identical case right.{"user_id": "alice", "OR": [{"user_id": "bob"}]}correctly returns[], becauseORis written to$oras a separate key and therefore still ANDs with the sibling. OnlyANDmerges into the same dict and clobbers. That inconsistency inside one function is hard to read as intentional.merge_filters; scalars still take the overwrite branch.What I Ruled Out
_process_metadata_filtersbefore the filter reaches any vector store, so it is not the Qdrant/FAISS/Valkey translation family (FAISS operator and $or/$not filters return no results #6424, bug(vector_stores/valkey): enhanced metadata filters silently become literal TAG matches #7206, bug(vector_stores/qdrant): two operators on one field apply only the first, so AND on one field returns excluded memories #7468).{"price": {"gt": 10}}+{"price": {"lt": 20}}; this is the scalar branch it does not reach. The TS twin of the dict case is TS OSS search() drops one bound of a same-field range filter (shallow Object.assign) #6260/fix(ts-oss): deep-merge same-field operators in advanced metadata filters #6261 and is also out of scope here.search()takes a single caller-builtfiltersdict with no separately trusted scope parameter. The one first-party merge point,server/main.py:455-461, assignsfilters["user_id"]after any client-suppliedAND, so the server's value wins and the scope holds — I tested that path specifically and it returned['alice']. Exploiting this would require a downstream application to merge untrusted input into its own scope dict and order the keys unfavorably, which is an application bug rather than a trust-boundary break in mem0.Suggested Fix
Make the scalar branch combine rather than replace, so a genuine contradiction yields no results instead of silently picking a winner — for example by folding a colliding scalar into an
eqoperator dict alongside the existing value, or by rejecting contradictory equality constraints outright. BothMemoryandAsyncMemorycopies would need it.AI Assistance
AI helped me find it, and I reproduced it myself afterwards