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

2 days 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
  1. 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.

    Slide text
    mem0
    The bug The fix The proof

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

    Python Node JS
    client.delete_all(user_id="")
    # before: DELETE /v1/memories/?user_id=
    # after:  ValueError, and no request is sent
  2. 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.

    Slide text
    mem0
    The bug The fix The 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
  3. 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.

    Slide text
    mem0
    The bug The fix The proof

    What the old Python client sent for five wrong inputs

    Call Request sent Result
    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
  4. 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.

    Slide text
    mem0
    The bug The fix The proof

    Two helpers raise before any request is sent

    mem0/client/main.py
    Python Node 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 ..."
                )
  5. 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.

    Slide text
    mem0
    The bug The fix The proof

    Each check runs where a wrong input does damage

    Sync and async
    Rejects filters
    Rejects a blank ID
    add()
    Checked
    Not checked a blank ID on add deletes nothing
    delete_all()
    Checked
    Checked
    delete_users()
    Not checked IDs are named arguments
    Checked
    delete_all(user_id="*") still deletes every user's memories, on purpose
  6. 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.

    Slide text
    mem0
    The bug The fix The proof

    The TypeScript client runs the same checks, camelCase keys included

    mem0-ts/src/client/mem0.ts
    Python Node 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.
  7. 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.

    Slide text
    mem0
    The bug The fix The proof

    AddMemoryOptions and DeleteAllMemoryOptions now take the four entity IDs

    mem0/client/types.py
    Python Node 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.
  8. 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.

    Slide text
    mem0
    The bug The fix The proof

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

    mem0/proxy/main.py
    Python Node 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.

  9. 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.

    Slide text
    mem0
    The bug The fix The 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
  10. 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.

    Slide text
    mem0
    The bug The fix The proof

    Valid calls still add and delete on a live project

    live check, throwaway project
    Python Node 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 live The empty-ID delete. It could wipe the project, so its "before" comes from an offline request capture.
  11. 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.

    Slide text
    mem0
    The bug The fix The proof

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

    Before client.add(messages, filters={"user_id": "alice"})
    After client.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.

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 ❤️