Skip to content

feat(memory): add batch_add, batch_update and batch_delete to Memory and AsyncMemory - #7599

Open
nothariharan wants to merge 1 commit into
mem0ai:mainfrom
nothariharan:feat/batch-memory-operations
Open

nothariharan wants to merge 1 commit into
mem0ai:mainfrom
nothariharan:feat/batch-memory-operations

Conversation

@nothariharan

Copy link
Copy Markdown

Linked Issue

Closes #3761

Description

Adds batch_add, batch_update and batch_delete to both Memory and AsyncMemory. The OSS surface had no batch methods, so callers had to loop add/update/delete one item at a time. The hosted client already shipped batch_update and batch_delete (mem0/client/main.py:692, :719), which confirmed the gap and supplied the payload shape to match.

Each method runs its items concurrently, but returns one entry per input item, in input order, and isolates per-item failures so a single bad item cannot take the batch down.

Two prior PRs were open against this issue and neither landed. Rather than pile on, this PR is built to be judged on its own, so here is where each one actually stands:

PR Status Why it did not land
#4804 open Broken on current main. 89 days with no activity, zero reviews, its only comment is the stale bot.
#5194 closed by kartik-mem0 2026-08-03 Conflicts with main plus 30+ days of inactivity; closed in a backlog cleanup.

#4804 does not compile on main

mem0/memory/main.py does not import List (from typing import Any, Dict, Optional at line 13) and has no from __future__ import annotations, so annotations are evaluated eagerly. Merging #4804 breaks import mem0 outright:

$ python -c "from mem0.memory.main import Memory"
IMPORT FAILED: NameError
MESSAGE: name 'List' is not defined
  File ".../mem0/memory/main.py", line 487, in <module>
    class Memory(MemoryBase):
  File ".../mem0/memory/main.py", line 1962, in Memory
    batch_data: List[Dict[str, Any]],
NameError: name 'List' is not defined. Did you mean: 'list'?

I reproduced this by applying #4804's diff onto current main (b7ad69af) in a throwaway worktree and importing it, so this is a measured result, not inferred.

#4804 also carried no tests (+237/-0, one file). It has been sitting unbuilt for 89 days because CI had nothing to run against it.

What this PR does differently

  • Compiles and is tested on latest main. Branch cut from b7ad69af, 417 existing tests still pass, plus 29 new ones.
  • Order is guaranteed. feat: add batch_add, batch_update, batch_delete to Memory and AsyncMemory (#3761) #4804 restored order with a sort keyed on id(input), which collapses on duplicate ids and returns a wrong default for anything unmatched. This version uses indexed futures, so batch_update([{"memory_id": "same"...}, {"memory_id": "same"...}]) stays aligned.
  • max_workers / max_concurrency escape hatch. My own end-to-end runs hit cannot commit - no transaction is active: Qdrant's local/embedded mode is not thread-safe (server and cloud Qdrant are). feat: add batch_add, batch_update, batch_delete to Memory and AsyncMemory (#3761) #4804 had no way to opt out, so it would have failed for every on-disk or embedded deployment.
  • Per-item errors carry enough context to find the item (memory_id or index), not just a message.

On the open design question

The triage note asks whether batch_add should run add() in parallel or batch the LLM extraction step. This PR takes parallel add() calls, because it keeps add()'s behaviour byte-identical and touches no extraction prompt. It is documented in the docstring and in docs/ that this costs one LLM extraction and one set of vector-store writes per item. If the maintainers want the batched-extraction variant instead, that is a much larger change and I would rather not smuggle it in here.

Type of Change

  • New feature (non-breaking change that adds functionality)

AI Assistance

  • AI-generated (an agent wrote most or all of this diff)

Tool: opencode (anthropic-backed). I read every line of this diff myself and I can explain each one and how it interacts with Memory.add/update/delete, SQLiteManager's locking, and the vector-store contract. What I checked manually: that SQLiteManager is thread-safe (check_same_thread=False plus a threading.Lock on every operation) before parallelising anything, and that the hosted client's batch_update/batch_delete payload shapes are the ones I mirrored. The two bugs my own testing caught (the unimported-List breakage in #4804, and the non-thread-safe local Qdrant writes) are both fixed here.

  • I can explain every line of this diff and how it interacts with the rest of the codebase, without asking an AI tool.

While the code was written with agent assistance, I have read and verified the full diff, run the tests myself, and can walk through the design decisions above.

Breaking Changes

N/A - purely additive. No existing signature or behaviour changes.

Test Coverage

  • I added/updated unit tests
  • I tested manually (describe below)

tests/memory/test_batch_operations.py - 29 cases covering input order preservation, batch-vs-per-item metadata merging (including not mutating the caller's dict), per-item error isolation, the max_workers / max_concurrency knobs, duplicate ids staying aligned, and a regression test asserting List is importable so the NameError that sank #4804 cannot come back.

$ pytest tests/memory -q
417 passed
$ ruff check mem0/memory/main.py tests/memory/test_batch_operations.py
All checks passed!

End-to-end verification

No mocked add/update/delete. Run against a real in-process Qdrant store (qdrant_client, no server) and a real deterministic embedder, so every memory was genuinely written, searched, updated and deleted:

1. seed 8 real memories individually (user_id='proofline')
   seeded ids     : 8
   get_all        : 8 memories

2. batch_add: 4 real writes, batch metadata merged under per-item metadata
   entries        : 4
   all succeeded  : True
   order preserved: ['I like oolong tea', 'I play chess on Sundays',
                     'I garden on weekends', 'I prefer dark roast']
   get_all        : 12 memories (8 seeded + 4 added)
   stored metadata: {'source': 'batch', 'topic': 'drinks'}

3. per-item error isolation: one failure must not kill the batch
   statuses       : [('success', None), ('error', 'simulated provider blowup'),
                     ('success', None)]

4. batch_update: real text rewrite, read back from the store
   update statuses: ['success', 'success', 'success']

5. batch_delete: real removes, roll-up reported accurately
   summary        : Deleted 6/6 memories
   remaining      : 8 memories

6. search: finds the rewritten memory
   0.700428  'REWRITTEN I like roasted barley tea'

7. ASYNC: batch_add / batch_update / batch_delete
   add statuses   : ['success', 'success', 'success']
   update statuses: ['success']
   delete summary : Deleted 2/2 memories
   rewritten row  : 'ASYNC REWRITTEN one'

8. concurrency proof: 6 items x 1s of simulated latency, wall clock
   6 items x 1s each -> batch_add took 1.07s (serial would be 6.00s)
   speedup           : 5.6x

ALL 8 PROOF STEPS PASSED

Steps 5-8 pass max_workers=1 / max_concurrency=1, because the Qdrant local mode backing store is the one that is not thread-safe. That is the documented remedy and the reason the knob exists.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have updated documentation if needed

…and AsyncMemory

The OSS Memory surface had no batch methods, so callers had to loop over
add/update/delete individually. The hosted client already shipped
batch_update and batch_delete (mem0/client/main.py), which confirmed the
gap and gave a payload shape to match.

Each method runs its items concurrently but returns one entry per input
item, in input order, and isolates per-item failures so a single bad
item cannot take the batch down.

max_workers (sync) / max_concurrency (async) caps concurrency, which is
what an on-disk or embedded vector store needs - Qdrant's local mode is
not thread-safe and concurrent writes raise 'cannot commit - no
transaction is active'.

Tests: 29 cases covering order preservation, metadata merging, per-item
error isolation, the concurrency knobs, and the List import that an
earlier attempt sank.

Also reorders the mem0.exceptions imports so pre-commit isort passes -
that block was already dirty on main, and touching this file requires it.
@github-actions github-actions Bot added documentation Improvements or additions to documentation sdk-python Python SDK specific labels Oct 10, 2026
@github-actions

Copy link
Copy Markdown

Hi @nothariharan, thanks for opening this pull request.

This is just a soft check: you are not yet in this repo's vouched contributor list (.github/VOUCHED.td). Nothing is blocked and there is nothing you need to do.

A maintainer can vouch for you by commenting !vouch @nothariharan on any issue.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation sdk-python Python SDK specific

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing Native Batch Operations in Memory and AsyncMemory Classes

1 participant