fix(client): reject empty entity IDs and filters in add and delete_all

2 hours ago

In the hosted mem0 clients, calling delete all with an empty user ID sent a request the server reads as every memory in the project. This pull request stops that call inside the client, in Python and TypeScript, before anything goes over the wire. It closes the same hole in delete users, and three quieter ones around the same arguments.

Ask about this presentation

Chapters

  1. 0:00An empty ID no longer wipes a project
  2. 0:22Where the API reads entity IDs
  3. 0:42What the old client sent
  4. 1:06Two helpers in the Python client
  5. 1:27Where each check runs
  6. 1:48The TypeScript client matches
  7. 2:10Typed options carry the entity IDs
  8. 2:36The proxy stops passing filters to add
  9. 2:58The tests failed first, then passed
  10. 3:20Checked against a throwaway project
  11. 3:41What changes for callers
Show transcript

An empty ID no longer wipes a project

mem0
The bugThe fixThe proof

An empty user ID no longer deletes
every memory in the project

PythonNode JS
client.delete_all(user_id="")
# before: DELETE /v1/memories/?user_id=
# after:  ValueError, and no request is sent

In the hosted mem0 clients, calling delete all with an empty user ID sent a request the server reads as every memory in the project. This pull request stops that call inside the client, in Python and TypeScript, before anything goes over the wire. It closes the same hole in delete users, and three quieter ones around the same arguments.

Where the API reads entity IDs

mem0
The bugThe fixThe proof

The API reads entity IDs in two different places

At the top level
add()
delete_all()
user_id="alice"
POST /v3/memories/add/ and DELETE /v1/memories/
Inside filters
search()
get_all()
filters={"user_id": "alice"}
Entity IDs are user_id, agent_id, app_id and run_id

The platform API reads entity IDs in two different places. Add and delete all read the user, agent, app and run IDs at the top level of the request. Search and get all read them inside filters. Both clients let callers mix the two up, and a blank ID went straight through to the server.

What the old client sent

mem0
The bugThe fixThe proof

What the old Python client sent for five wrong inputs

CallRequest sentResult
delete_all(user_id="")
DELETE /v1/memories/?user_id=
Deletes every memory
delete_users(user_id="")
GET /v1/entities/, then DELETE user/alice, DELETE agent/bot
Deletes every entity
delete_all(filters={"user_id": "alice"})
DELETE /v1/memories/?filters=%7B%27user_id%27%3A…
Deletes nothing
add("hi", AddMemoryOptions(user_id="alice"))
POST /v3/memories/add/ {"messages":[...]}
Drops user_id
add("hi", filters={"user_id": "alice"})
POST /v3/memories/add/ {…,"filters":{...}}
400 from the server

Here is what the old Python client put on the wire. Two calls could wipe data. Delete all with an empty user ID, and delete users with one, which fell through a truthiness check and deleted every entity. Three more failed quietly. A filters dict on delete all went out as a string and deleted nothing, the typed add options dropped the user ID, and filters on add got a 400.

Two helpers in the Python client

mem0
The bugThe fixThe proof

Two helpers raise before any request is sent

mem0/client/main.py
PythonNode JS
def _reject_filters(method: str, params: Dict[str, Any]) -> None:
    if params.get("filters") is not None:
        raise ValueError(
            f"filters is not supported in {method}(). Pass entity IDs at the top level, ..."
        )

def _reject_blank_entity_ids(params: Dict[str, Any]) -> None:
    for name, value in params.items():
        if name in ENTITY_PARAMS and isinstance(value, str) and not value.strip():
            raise ValueError(
                f"{name} cannot be empty or whitespace-only: an empty ID would match every ..."
            )

The Python fix is two module level helpers. The first raises a value error whenever filters is set. None counts as not provided, because the proxy always passes filters as None. The second raises on any entity ID that is empty after stripping whitespace. A missing ID passes, and so does the star wildcard.

Where each check runs

mem0
The bugThe fixThe proof

Each check runs where a wrong input does damage

Sync and async
Rejects filters
Rejects a blank ID
add()
Checked
Not checkeda blank ID on add deletes nothing
delete_all()
Checked
Checked
delete_users()
Not checkedIDs are named arguments
Checked
delete_all(user_id="*") still deletes every user's memories, on purpose

Each check runs in the sync and the async client. Delete all gets both checks. Delete users gets the blank ID check. Add only rejects filters, because a blank ID on add can't delete anything, so it is left alone. And the star wildcard still works. It stays the explicit way to delete everything.

The TypeScript client matches

mem0
The bugThe fixThe proof

The TypeScript client runs the same checks, camelCase keys included

mem0-ts/src/client/mem0.ts
PythonNode JS
async deleteAll(
  options: DeleteAllMemoryOptions = {},
): Promise<{ message: string }> {
  rejectFilters(options, "deleteAll");
  rejectBlankEntityIds(options);
  ...
}
karthik-indla, in review
“Please confirm the TS side also has the blank/whitespace entity-ID guard with identical semantics.”
It does. Same four fields plus userId, agentId, appId and runId. Same trim rule, "*" passes, and the error text matches.

On the TypeScript side, the same two helpers sit next to the existing top level check. They run in add, delete all and delete users, before any fetch. In review, the question was whether the two clients really agree on blank IDs. They do. Same four fields, plus the camelCase forms, the same trim rule, and the same error text pointing at the star.

Typed options carry the entity IDs

mem0
The bugThe fixThe proof

AddMemoryOptions and DeleteAllMemoryOptions now take the four entity IDs

mem0/client/types.py
PythonNode JS
class AddMemoryOptions(BaseModel):
    filters: Optional[Dict[str, Any]] = Field(...)
    user_id: Optional[str] = Field(default=None, ...)
    agent_id: Optional[str] = Field(default=None, ...)
    app_id: Optional[str] = Field(default=None, ...)
    run_id: Optional[str] = Field(default=None, ...)
    metadata: Optional[Dict[str, Any]] = Field(...)
Known gap
Extra fields are still allowed, so AddMemoryOptions(filters=...) is ignored without an error, and the server returns the same 400 as before.

The typed options had the contract backwards. Their docstrings said IDs go inside filters, so add memory options with a user ID, the call the migration guide shows, dropped it. The filters field is gone from both models, and the four entity IDs replace it. One gap is left. The models still allow extra fields, so a filters argument there is ignored, and the server answers with the same 400 as before.

The proxy stops passing filters to add

mem0
The bugThe fixThe proof

The proxy now sends filters to search() and keeps them out of add()

mem0/proxy/main.py
PythonNode JS
- self._async_add_to_memory(messages, user_id, agent_id, run_id, metadata, filters)+ self._async_add_to_memory(messages, user_id, agent_id, run_id, metadata)  relevant_memories = self._fetch_relevant_memories(messages, user_id, agent_id, run_id, filters, top_k)

Also fixed: the Python example in docs/platform/features/custom-instructions.mdx now passes agent_id="support-agent" instead of filters.

A sweep of internal callers found two that passed filters to add. The proxy's chat completions forwarded filters to add from a daemon thread, where the new error would be raised and lost. Filters now go only to search, and a regression test pins that. The second was the custom instructions docs example, which now passes the agent ID directly.

The tests failed first, then passed

mem0
The bugThe fixThe proof

The new tests failed first, then passed

Python
58 failed
of 66 new guard tests, before the fix
→
108 passed
guard tests plus tests/test_client.py
TypeScript
40 failed
of 47 new guard tests, before the fix
→
1,606 passed
full mem0-ts suite, 42 skipped

The tests were written before the fix. In Python, 66 new guard tests, and 58 of them failed against the old client. After the fix, they pass together with the existing client tests, just over a hundred in all. In TypeScript, 47 new tests, 40 failing before. After the fix, the whole suite passes, about 1,600 tests.

Checked against a throwaway project

mem0
The bugThe fixThe proof

Valid calls still add and delete on a live project

live check, throwaway project
PythonNode JS
add(filters={"user_id": ...})                    -> ValueError: filters is not supported in add()
add(AddMemoryOptions(user_id=..., infer=False))  -> 'status': 'SUCCEEDED'
memory visible for user via get_all(filters): True count: 1
delete_all(DeleteAllMemoryOptions(user_id=...))  -> 'Delete in progress.'
memories remaining for user after delete_all: 0
Not run liveThe empty-ID delete. It could wipe the project, so its "before" comes from an offline request capture.

Then a live run, against a throwaway project with a one-off user ID. Filters on add now raise. Add with the typed options stores the memory, get all finds exactly one, and delete all with the typed options leaves zero. The TypeScript run matched. The empty ID delete was kept off the live project on purpose, since it could wipe it.

What changes for callers

mem0
The bugThe fixThe proof

Pass entity IDs at the top level, and use "*" to delete on purpose

Beforeclient.add(messages, filters={"user_id": "alice"})
Afterclient.add(messages, user_id="alice")
The server still matches every memory on an empty ID. Its fix is out of scope here; only the two hosted clients are guarded.
Version bump in a separate pull request: Python SDK 2.3.0 and TypeScript SDK 3.4.0, with the new errors listed under Breaking Changes.

For anyone calling the clients, the change is one line. Entity IDs go at the top level of add and delete all, never in filters. The server still treats an empty ID as every memory and needs its own fix, and the version bump lands in a separate pull request. Until then, the clients are the guard, and an empty user ID no longer deletes every memory in the project.

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️