Repository navigation
Conversation
This was referenced Oct 20, 2026
[qdrant options] Let a deployment tune a Qdrant collection's HNSW, optimizers and quantization
#1618
Draft
Draft
Open
Draft
[event memory 2/6] Rename the segment store to the event memory store (port of #1659's rename)
#1686
Draft
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
from
October 26, 2026 18:32
52e1805 to
5b16f04
Compare
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
5 times, most recently
from
October 28, 2026 23:25
b631cd8 to
f4b4870
Compare
Contributor
Author
|
Need performance measurement. |
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
3 times, most recently
from
November 2, 2026 17:57
0154c69 to
96097c2
Compare
EventMemory is built from the segment store, the vector store and the models. It opens or creates a partition by key in both stores, the vector store's first, and deletes a partition from both. EventMemoryPartition is the handle that encodes, queries and forgets; it holds its two store partitions privately, so no caller can pair one key's partition in one store with another key's partition in the other. The service locator builds the EventMemory and opens the session's partition through it. EventBackendParams carries the EventMemory and the handle, and LongTermMemory deletes the session's partition through EventMemory. The reserved timestamp field and text extraction, shared by both classes, move to module level. No behavior changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion A vector store's schema is fixed when the store is built, so EventMemory takes it as a parameter, the same schema the composition root built the vector store with, rather than reading it from each partition it opens. - EventMemoryParams gains `store_schema`. EventMemory checks once, at construction, that the schema declares the fields EventMemory requires, and hands each partition handle the declared fields and the similarity metric. A handle's `similarity_metric` and its score direction come from them. - The service locator builds the vector store and EventMemory from one schema, `event_backend_store_schema`. - `VectorStorePartition` no longer has a `store_schema` property; each store's partitions keep the schema privately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
from
November 2, 2026 19:34
96097c2 to
cda5fac
Compare
Deleting a session built its EpisodicMemory, which opened, and so could create, the session's event-backend partitions, and then deleted them through the session's own LongTermMemory, which held the EventMemory only for that. The long-term service locator now builds each backend's memory and deletes a session's storage, both as functions that match on the configuration's backend: the event backend deletes the session's partitions by key through EventMemory, opening neither, and the declarative backend deletes the episodes its DeclarativeMemory of the session matches. The episodic service locator deletes the storage whenever the configuration builds long-term memory. EpisodicMemoryManager closes a cached idle instance, then deletes the stored configuration's storage, and builds no EpisodicMemory. LongTermMemory is the one class that routes requests on the backend: it takes each backend's memory built, holds no EventMemory, partition key, or deleted state, and has neither drop_session_partition nor close, whose body did nothing. EpisodicMemory.delete_session_episodes has no caller and goes; its short-term half only reset in-memory state. The evaluation scripts build the DeclarativeMemory they pass in, and delete a session through delete_long_term_memory_storage. A session saved with `enabled: false` now has its long-term storage deleted too, where the old path returned before deleting anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LongTermMemory has had a single search since its unscored `search` went, so `search_scored` no longer tells it apart from anything. It is `query`, as the calls above and below it are: EpisodicMemory.query_memory and EventMemoryPartition.query. Its two backend branches are `_query_declarative` and `_query_event`. DeclarativeMemory keeps `search_scored`, which it tells apart from its unscored `search`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
from
November 2, 2026 23:55
cda5fac to
6a90736
Compare
`_declarative_params` was one statement and `_event_params` a block with one caller, each standing between `long_term_memory_params_from_config`'s match and the work it routes to. The work now sits in the match arms, as it does in `delete_long_term_memory_storage` beside it. `_declarative_memory` and `_event_memory` stay, since building and deleting both use them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Once the service locator built each backend's memory, the two backend params differed only in that memory: a DeclarativeMemory, or an EventMemoryPartition with the episode storage that turns its results into episodes and the reranker that sets its score direction. LongTermMemory now takes the memory itself, with those two as keywords, and each request matches on the memory's type. An EventMemoryPartition without episode storage is refused when the LongTermMemory is built. DeclarativeBackendParams, EventBackendParams, their union, the backend literal, and the session id, which nothing read, go. The service locator's `long_term_memory_from_config` builds the LongTermMemory in each match arm, and the episodic service locator takes it from there. The evaluation helper passes its DeclarativeMemory directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The episodic service locator's `delete_episodic_memory_storage` only checked whether the stored configuration builds long-term memory and passed it on. EpisodicMemoryManager.delete_episodic_session now makes that check and calls `delete_long_term_memory_storage` itself, so deleting a session goes from the manager straight to the long-term layer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
from
November 3, 2026 01:40
6a90736 to
ea69c3a
Compare
EventMemoryParams carried the schema its vector store was built with, beside the store itself, so the schema had two sources that the composition root had to keep in step. A vector store now answers its own `store_schema`, the schema it was built with, and EventMemory reads its required fields, declared fields, and similarity metric from it. EventMemoryParams no longer takes a schema, and the service locator no longer passes one. Each store returns the schema it already held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event memory bounds when its vector writes are sent by cancelling an upsert at a deadline, which needs a written guarantee of what cancelling does. VectorStorePartition.upsert now states that cancelling the call stops it from sending further requests to the backend, and that a request already sent may still be applied, so a cancelled upsert may have written some of the records. RegistryBackedVectorStorePartition's `_upsert` hook states the same obligation for its implementations. Every implementation honors it: - RegistryBackedVectorStorePartition awaits the registration check, `_upsert`, and the check again in sequence, so a cancellation stops at the await it lands on. - Qdrant and Milvus await each request, and send the halves of a batch the server refuses as too large one after another, so a cancellation raised in a request ends the call. Neither client catches CancelledError: qdrant-client's REST path catches Exception only, and pymilvus's retry decorators catch grpc.RpcError, MilvusException, and Exception, so a cancelled RPC surfaces as CancelledError and is not retried. - sqlite-vec writes in one transaction. A statement already queued to aiosqlite's thread finishes there; SQLAlchemy treats the cancellation as a disconnect and invalidates the connection, so the transaction never commits and nothing more is executed on it. - The engine-backed SQLite store commits its records and pending operations, then applies them to the search engine, then marks them applied. A cancellation in the engine step leaves the operations pending, and startup replays them. - The in-memory test partition has no await in its upsert. Tests cancel an upsert mid-call and check that nothing more goes out: for Qdrant and Milvus with a fake client that refuses the batch as too large and holds its first half (the second half is never sent, and the registration is not checked after it), and against the server (the request in flight; for Milvus counted at the gRPC stub, so a pymilvus retry would show); for both SQLite stores, cancelling at the upsert's first statement, and for the engine-backed one also while its search engine applies the records, no further statement executes. Verification: ruff, ruff format, and ty clean; the vector store unit tests, 401 passed, 5 skipped. The fake-client tests fail when the halving swallows the cancellation and sends the second half. The integration tests need Docker and were not run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…forgets The segment store and the vector store share no transaction, so an ingest that stopped after storing its events left them unsearchable, and a forget racing an ingest or a late vector write could leave vectors of forgotten events behind. The segment store now holds the events themselves, and two queues of work the vector store still owes: - Pending derivatives: an ingest stores its events, segments, derivative links, and one pending record per event in one transaction, upserts the vectors within a write budget, and deletes the record. A record left behind is claimed by any process once the budget has passed, and written again within a budget of its own. - Tombstones: a forget locks its events, deletes their pending records, records the derivatives it unlinked in one tombstone, and deletes the events in one transaction. The vectors are deleted at once and again after the tombstone retention, which outlasts any write sent within its budget. Claims hold a lease, back off doubling on failure, and stop after repeated failures, which is logged. A search drops a hit with no link and deletes its vector once, best effort. Partitions are deleted from the segment store first. Each purge call stays within its bound across segments, events, and the derivatives tombstones name. design/event_memory_ingest_and_deletion.md gives the protocol and why it is safe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EventMemory's background work claims pending derivatives and tombstones from the whole segment store database, and its records name no vector store or embedder: it writes and deletes with its own. That is right only if one EventMemory serves every partition in the database, and only if it runs whether or not a session is opened. The resource manager now builds the deployment's EventMemory from the episodic memory's long-term memory configuration, starts its background work, and shuts it down on close. The server builds it at startup when that configuration names a segment store. A session opens its partition on it, and the deployment's segmenter, deriver, and reranker serve every session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…deployment The deployment's one EventMemory serves every event session with its own segmenter, deriver, and reranker, so the copies a session saved no longer apply. The session's event configuration no longer has those fields, and rows saved earlier load with them ignored; creating a project checks a reranker only on the declarative backend. A project's `reranker` applies to the declarative backend only, as its description now says, and an event project's response names none, as for `embedder`. The OpenAPI document is regenerated for these fields only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding events and claiming pending derivatives and tombstones depend on RETURNING, which SQLite added in 3.35: the event insert returns the events it stored, and each claim returns the row it took. The store's params refuse an older SQLite runtime, after the check that each session gets its own connection to one database, and a test raises the minimum past the runtime to see the refusal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
edwinyyyu
force-pushed
the
feat/event-memory-recovery
branch
from
November 3, 2026 17:22
ea69c3a to
a82fc6e
Compare
Contributor
Author
|
Should simplify this. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose of the change
Event memory keeps events, segments, and derivative links in the SQL segment store, and the derivatives' vectors in the vector store. The two share no transaction, so:
This PR makes ingests complete and forgets complete, with no lock held across a vector store call.
design/event_memory_ingest_and_deletion.mdgives the protocol, the alternatives, and why it is safe.Behavior that changes:
rerankerapplies to the declarative backend only, asembedderdoes, and an event project's response names none. The OpenAPI document and the Python client's docstrings say so.The safety argument relies on what cancelling an upsert guarantees, which this PR's first commit states in
VectorStorePartition.upsert's contract: cancelling the call stops it from sending further requests, and a request already sent may still be applied. Every store already behaves that way. Each store's tests cancel an upsert mid-call and check that nothing more goes out: Qdrant and Milvus with a fake client that refuses the batch as too large and holds its first half, and against the server; both SQLite stores at the upsert's first statement, and the engine-backed one also while its search engine applies the records.The protocol was model-checked on a small instance (two encodes of one event, an evicting encode, two recovery workers, crashes and timeouts at every step, forgets at any moment). Removing any one piece breaks deletion, findability, completion, durability, or erasure.
Stack
24 open PRs: three independent PRs, and the vector store tree of short parallel branches. Every open PR in the tree has
feat/horizontal-scalingas its GitHub base, and the independent PRs havemain. The branches are in a fork, and a pull request can target only this repository's branches, so the on column gives the order the PRs build on each other. A stacked PR's diff on GitHub includes the PRs under it until they merge.Independent of the vector store tree, directly on
main:mainmainmainThe vector store tree. Each PR builds on the one in its on column; PRs on the same parent are parallel branches and do not depend on each other. #1631 is closed, superseded by #1733–#1736, which hold its changes split in four, with review changes since. #1675 is closed, superseded by #1627. #1841 is closed: its request packing is dropped, and its cancellation contract is in #1840. #1828 and #1839 sit side by side on the branch; #1839 and #1840 are the event memory recovery stack, and #1684 heads the event memory stack, which its own PRs index, listed because it builds on #1840, its branch also carrying #1828's commits until one of the two merges; #1871 keeps its own Stack section. Until the PRs under it merge, their changes show in a stacked PR's diff.
mainfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingmemmachine-server migrate, which creates every configured store's schema before servers startfeat/horizontal-scalingThis PR is its 5 commits,
739ac7ec1,90c9761c2,943ddfacb,552ed2d77,a82fc6e65, stacked on #1839. #1684 is stacked on it.Verification
At this PR's head
a82fc6e65:ruff checkandruff format --checkare clean,ty checkis clean as CI runs it (uv run --frozen --all-extras ty check --project packages/server), anduv lock --checkpasses.packages/server/server_tests -m 'not integration'): 2415 passed, 8 skipped.packages/client/client_tests): 259 passed.LongTermMemorytests against Neo4j, 3 passed; the vector store tests against Qdrant, Milvus, and the registry's PostgreSQL, 387 passed.🤖 Written by Claude Code (Claude Opus 5.5) on behalf of @edwinyyyu.