Thanks to visit codestin.com
Credit goes to github.com

Skip to content

New long-term episodic memory (named EventMemory to differentiate) based on VectorStore and (new) SegmentStore#1205

Merged
malatewang merged 102 commits into
MemMachine:mainfrom
edwinyyyu:sqlalchemy_segment_linker
May 8, 2026
Merged

New long-term episodic memory (named EventMemory to differentiate) based on VectorStore and (new) SegmentStore#1205
malatewang merged 102 commits into
MemMachine:mainfrom
edwinyyyu:sqlalchemy_segment_linker

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Mar 10, 2026

Copy link
Copy Markdown
Contributor

Purpose of the change

Motivation:

  • current DeclarativeMemory does not handle multimodal content
  • current DeclarativeMemory does not handle large messages or text items well (no chunking).
  • current DeclarativeMemory produces high fan out in number of Neo4j queries
  • current DeclarativeMemory operations do not tolerate failures
  • VectorGraphStore is difficult to implement using other databases
  • we have a new VectorStore interface defined previously to move toward a solution
  • tangential: current DeclarativeMemory may face problems with top-k redundancy from vector search
  • current DeclarativeMemory has no way of distinguishing name from identity
    • top-level API from server does not provide enough information as there is no concept of temporal context
  • add encryption support (initial infrastructure)

Description

For wiring into server, #1304 is required.

Choices:

  • JSON/JSONB allows faster upserts. Filters are already scoped to queries so queries are fast. GIN indexes are not so useful right now. Another alternative is EAV properties table which requires more complexity but can be better for range filters.

Changes:

  • define new extensible data models to support multimodal content and chunking
  • define new APIs to allow for more efficient and atomic operations
  • API: limit is now vector limit for transparency and to avoid throwing away computations -- it is trivial for client to limit/threshold and transform into usable context and this allows much more flexible iterative expansion without requerying
  • uses new API to avoid breaking existing memory for now -- although I would prefer an overhaul of the server due to its many problems [Feat]: Server tech debt resolution wishlist #1297
    - introduce derivative eviction system for duplicate-heavy online-ingested data (cannot pre-dedup, need to maintain index quality)

each segment comes from exactly one episode
each derivative comes from exactly one segment

alternative considered: many-to-many segments-to-derivatives

  • slows growth of vector index
  • handles HNSW index degradation due to ((potentially) near) duplicates
  • rejected for complexity and inability to filter in vector store
  • consolidation is about as effective as increasing the search limit proportionally
  • requires reference counting, ownership transfer, or similar

Approach to deletion:

  • purging state necessary as a lock to allow deleting from external DB (vector DB)

Scoping:

  • by partition key: should allow sharding and horizontal scaling

  • this memory will be parallel to DeclarativeMemory

Decisions to make:

  • naming

POC:

  • Current Neo4j implementation on a single MacBook Pro (M3 Pro, 18GB) cannot handle even 30 concurrent search queries with search limit 100 (latency explodes).
  • Qdrant + PostgreSQL can handle 50+ concurrent search queries with acceptable latency.

Type of change

[Please delete options that are not relevant.]

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactor (does not change functionality, e.g., code style improvements, linting)
  • Documentation update
  • Project Maintenance (updates to build scripts, CI, etc., that do not affect the main project)
  • Security (improves security without changing functionality)

How Has This Been Tested?

  • Unit Test
  • Integration Test
  • End-to-end Test
  • Test Script (please provide)
  • Manual verification (list step-by-step instructions)

Checklist

  • I have signed the commit(s) within this pull request
  • My code follows the style guidelines of this project (See STYLE_GUIDE.md)
  • I have performed a self-review of my own code
  • I have commented my code
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added unit tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules
  • I have checked my code and corrected any misspellings

Maintainer Checklist

  • Confirmed all checks passed
  • Contributor has signed the commit(s)
  • Reviewed the code
  • Run, Tested, and Verified the change(s) work as expected

@edwinyyyu edwinyyyu force-pushed the sqlalchemy_segment_linker branch 5 times, most recently from a724574 to ced2263 Compare March 10, 2026 19:00
@edwinyyyu edwinyyyu mentioned this pull request Mar 10, 2026
26 tasks
@edwinyyyu edwinyyyu force-pushed the sqlalchemy_segment_linker branch 6 times, most recently from 5498137 to e540bde Compare March 13, 2026 00:05
@edwinyyyu edwinyyyu force-pushed the sqlalchemy_segment_linker branch from e540bde to a6954a9 Compare March 13, 2026 17:35
@edwinyyyu edwinyyyu force-pushed the sqlalchemy_segment_linker branch 10 times, most recently from 5d451f6 to 29d090d Compare March 18, 2026 03:30
@edwinyyyu

Copy link
Copy Markdown
Contributor Author

Still needs some performance fixes.

@edwinyyyu

Copy link
Copy Markdown
Contributor Author

Maybe lazy ownership transfer is more performant.

@edwinyyyu edwinyyyu force-pushed the sqlalchemy_segment_linker branch from 29d090d to e3961e2 Compare March 20, 2026 00:49
@edwinyyyu

edwinyyyu commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

Wiring for SQLAlchemySegmentStore will be added in #1304 if this is merged.

class MessageContext(BaseModel):
"""The content is communicated by a source."""

type: Literal["message"] = "message"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is clearer to use messagecontext

@edwinyyyu edwinyyyu May 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to be ProducerContext and context_type: "producer". Removed CitationContext (no use case yet).

Having "context" in the value for the discriminator is redundant and more error-prone than "context" in the discriminator key.

segment_uuid: UUID
timestamp: datetime
context: Context = Field(default_factory=NullContext)
text: str

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to use block instead of plain str here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original intent was for Derivatives to be text-only since multimodal embedding models are uncommon. The solution for supporting multimodal embeddings is probably to make Derivatives text if the embedding reports no support for multimodality.

f"{', '.join(sorted(reserved_fields))}"
)

missing_fields = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been checked in the constructor

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This checks that input events for ingestion are not missing fields, which cannot be done at construction time.

Unless you mean it is checked at Event construction time? In that case I think it's safer to check close to where the condition is required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think the code is working that way. It does not check the property of the incoming events.

@edwinyyyu edwinyyyu May 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I think that (or checking that the schema includes all fields required by events) was the behavior at some point but it changed and it was not deduplicated. I will remove.


events = sorted(
events,
key=lambda event: (event.timestamp, event.uuid),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the UUID is random, there is no sense to sort by uuid here. Or do we require special UUID generator?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implied behavior is that UUID can be used to order events that have the same timestamp, so behavior with UUIDv7 vs. UUIDv4 works differently.

This is not documented, so I can remove it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the original motivation for sorting at all was to make temporal derivative eviction easier -- simulating the order of occurrence.

"""Event memory system."""

# System-defined metadata field names. Reserved.
_SEGMENT_UUID_FIELD_NAME = "_segment_uuid"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are they enough? Event source should be indexed

@edwinyyyu edwinyyyu May 5, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event source (along with all Context fields) is LLM-friendly private data that should not be indexed in a multi-tenant system where we may have encryption. If the user/consumer feels that they need to filter on this and it is not sensitive information, they may add it to the properties schema.

It is safer to completely prohibit indexing anything in Context because Context can reveal things like the user's name, interests (if adding content from something like a book), sensitive information like location and time they were there (which may reveal where they live), etc in plaintext (which may be required for most/all filters to work).

If it needs to be system-defined, the application may add _producer_id as a non-LLM-friendly system-reserved field in properties (EventMemory is not responsible for reserved prefix validation, and it allows for creating upper-level system-reserved fields.). In this proposed case, producer_id is a property that exposes no sensitive information (it is just a user or agent id like user_123 or a UUID or something).

Another challenge that we avoid by not indexing this is that semantically different fields (potentially with different types) with the same name do not collide. Because Context is a discriminated union, enforcing this would be complicated and each Context implementation would have to consider what other implementations have already reserved.

Originally I agreed with this idea. But the above considerations made me reconsider. See 41f2fc1, 7582233 for changes. Also the code was a lot more complicated when supporting filtering by context.

@edwinyyyu edwinyyyu mentioned this pull request May 6, 2026
f"{', '.join(sorted(reserved_fields))}"
)

missing_fields = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think the code is working that way. It does not check the property of the incoming events.

t_embed - t_derive,
t_seg_store - t_embed,
t_v_store - t_seg_store,
t_v_store - t_start,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the stats to metrics

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Renamed/reorganized.

label_names=("phase",),
)

self._text_splitter = RecursiveCharacterTextSplitter(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our discussion, I think we agreed to move this out of the event memory

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

edwinyyyu added 3 commits May 6, 2026 16:12
Signed-off-by: Edwin Yu <[email protected]>
Signed-off-by: Edwin Yu <[email protected]>
Signed-off-by: Edwin Yu <[email protected]>
@edwinyyyu

Copy link
Copy Markdown
Contributor Author

Before merge, I would like to do another manual verification run on LoCoMo/LongMemEval, since there have been quite a few changes since last runs.

@edwinyyyu edwinyyyu mentioned this pull request May 7, 2026
26 tasks
@edwinyyyu

edwinyyyu commented May 7, 2026

Copy link
Copy Markdown
Contributor Author

LoCoMo score is good enough. Used SQLite for both segment store and vector store (sqlite-vec). Happy path works.

@edwinyyyu

Copy link
Copy Markdown
Contributor Author

No breaking changes since 0.3.7 so I'm changing the milestone to 0.3.8. Wiring determines if 0.4.0 is needed.

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

Labels

Integration An integration into a 3rd party framework or solution using MemMachine performance Issues relating to MemMachine performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants