Skip to content

[runtime][api] Scope long-term memory operations to the action that obtained the memory set - #1002

Open
weiqingy wants to merge 1 commit into
apache:mainfrom
weiqingy:998-bind-key-to-memoryset
Open

[runtime][api] Scope long-term memory operations to the action that obtained the memory set#1002
weiqingy wants to merge 1 commit into
apache:mainfrom
weiqingy:998-bind-key-to-memoryset

Conversation

@weiqingy

Copy link
Copy Markdown
Collaborator

Linked issue: #998

Purpose of change

Mem0LongTermMemory is shared across partition keys and holds the current key, observation id and suppression flag in mutable fields. switch_context writes them on the mailbox thread before each action, while add, get, search and delete can run on a worker thread when an action passes them to durable_execute_async, and those operations read the fields themselves. The key they see is whatever the mailbox thread wrote most recently, not the key of the action that submitted the work.

The key is the isolation boundary rather than observability metadata: it reaches Mem0 as agent_id, and two keys sharing a job id and a memory set name are separated by agent_id alone. Reading the wrong one means one key's items land in, or are read from, another key's set. The observation id and suppression flag come from the same fields, so observations can be misattributed independently.

This binds the partition key, observation id and suppression flag onto the MemorySet when it is created, and has the four operations take them from the set. The four already receive memory_set as their first parameter, so nothing new is threaded through. A set is consequently scoped to one action and must not be held across actions.

Java is covered in the same change rather than separately. Its operations all delegate into the same Python object, and the bridge rebuilt the Python set from its name alone, so once the Python side sources the key from the set, a Java-originated call would arrive without one. The Java wrapper now records the context when it switches and carries it across the bridge, and to_python_memory_set requires the key rather than defaulting it.

Operating on a set that carries no binding raises. Mem0 tests agent_id for truthiness instead of matching on it, so an unbound set would widen an operation to every key sharing the job id and set name, which for a delete removes another key's items. An empty string is a legal partition key and is unaffected: the check is on None and null only.

delete_memory_set is unchanged. It takes a name rather than a MemorySet, so it has no bound context and applies to the key currently in scope, which its documentation now records along with the fact that it can target a different key than MemorySet.delete on a same-named set. Giving it the same isolation means changing its signature, which is a public API question left open on #998.

Tests

Three new tests and two adapted, plus one new Java test.

Test What it pins
test_memory_set_stays_on_its_own_key_after_the_owner_switches all four operations keep the set's key after the owner switches
test_observations_stay_with_the_action_that_obtained_the_set observation ownership follows the set for add, get, search and delete
test_suppression_follows_the_set_not_the_current_context the set's own suppression flag decides, in both directions
test_unbound_memory_set_is_refused_rather_than_widened all four operations raise, and none reaches Mem0
testForwardedSetCarriesTheContextItWasObtainedIn the Java bridge forwards the context bound at creation
testUnboundSetIsRefusedRatherThanWidened the Java guard throws before forwarding

test_switch_context and test_context_switch_changes_observation_owner_and_current_suppression reused a single set across a context switch and expected the current key to apply. They now assert that a set keeps its own key, and obtain one per context.

Each assertion was checked against a deliberately reverted implementation: reverting the suppression binding, the observation id binding, either guard body, or the key binding itself each leaves at least one test failing, and reverting the key binding fails five including both parameterisations of test_switch_context.

122 Python tests pass across api/memory, runtime/memory and runtime/tests. 75 Java tests pass across MemorySetTest, Mem0LongTermMemoryTest, MemoryRefTest, TestMemoryObservationFlush, ActionTaskContextManagerTest and ActionExecutionOperatorTest. ruff check, ruff format --check and spotless:check are clean.

The long-term memory e2e test is gated on ACTION_API_KEY and needs a Flink cluster plus a local Ollama embedding model, so end-to-end coverage comes from CI.

API

MemorySet gains three fields in both languages, excluded from serialization like the existing long term memory back-reference. Java adds setActionContext and three getters. BaseLongTermMemory.get_memory_set / getMemorySet keeps its signature and gains the documented per-action scope. The runtime bridge helper to_python_memory_set now requires the partition key; its only caller is the Java wrapper, and a version mismatch fails loudly with a missing-argument error rather than silently.

Existing code that obtains a memory set inside the action using it is unaffected. Code that cached one across actions was already reading whichever key happened to be current, and now raises or stays on its original key instead.

Documentation

  • doc-needed
  • doc-not-needed
  • doc-included

…btained the set

Mem0LongTermMemory holds the partition key and observation context in mutable
fields that the mailbox thread overwrites on every context switch. Operations
submitted through durable_execute_async run on a worker thread and read those
fields themselves, so an operation can be attributed to whichever key the
mailbox thread most recently switched to. The key reaches Mem0 as agent_id,
which is the only isolation boundary between two keys that share a job id and
a memory set name.

Bind the partition key, observation id and suppression flag onto the MemorySet
when it is created on the mailbox thread, and have add, get, search and delete
take them from the set. The Java wrapper forwards into the same Python object
and rebuilt the Python set from its name alone, so it now records the context
on switch and carries it across the bridge.

A set is therefore scoped to one action and must not be reused across actions.
Two existing tests reused one across a context switch and expected the new key
to apply; they now assert that the set keeps its own key instead.

Operating on a set that carries no binding raises rather than proceeding. Mem0
ignores a falsy agent_id instead of matching on it, so an unbound set would
widen an operation to every key sharing the job id and set name, which for a
delete would remove another key's items.

delete_memory_set takes a name rather than a MemorySet and still reads the
shared field, which its docstring now records.

Generated-by: Claude Code 2.1.228
@github-actions github-actions Bot added doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue. labels Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc-included Your PR already contains the necessary documentation updates. fixVersion/0.4.0 priority/major Default priority of the PR or issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant