Repository navigation
Conversation
edwinyyyu
marked this pull request as draft
September 14, 2026 19:06
edwinyyyu
force-pushed
the
feat/filter-union-speedkick
branch
5 times, most recently
from
September 14, 2026 20:33
e5f728a to
bdadfe0
Compare
This was referenced Sep 14, 2026
Closed
edwinyyyu
force-pushed
the
feat/filter-union-speedkick
branch
from
September 14, 2026 21:42
bdadfe0 to
f8ca110
Compare
edwinyyyu
force-pushed
the
feat/filter-union-speedkick
branch
5 times, most recently
from
September 14, 2026 23:16
219905a to
792c4e9
Compare
edwinyyyu
force-pushed
the
feat/filter-union-speedkick
branch
2 times, most recently
from
September 15, 2026 17:26
219905a to
ed99d4b
Compare
This was referenced Oct 14, 2026
Open
Draft
…kend The filter language is a closed union: Equals, Ordering, In, IsNull, n-ary And and Or, and Not. Every compiler is an exhaustive match. The parser maps `!=` and `<>` to Not(Equals), `NOT IN` to Not(In) and `IS NOT NULL` to Not(IsNull), so a negation means one thing. A leaf on a field holding no value is false, IsNull on it is true, and Not is the ordinary complement over the record set. The SQL compilers render Not as `NOT COALESCE(x, FALSE)`; they compiled `~inner`, which is three-valued and dropped rows holding no value. Milvus already pushes Not to the leaves by De Morgan, since its own `not` excludes entities lacking the field, and Qdrant's must_not already admits them. A predicate matches only a value of the compared type. The graph stores and the Neo4j semantic storage get the union only; their NOT over a comparison on a missing property stays three-valued, since no backend here can test a change. Where the compiled output changes: common/filter/sql_filter_util.py and common/vector_store/sqlite_properties.py (COALESCE under Not), and the parser's mapping of `!=`. Where behavior changes: the in-memory test collection's evaluator matches values by type, the declared-schema contract gains the complement law and the typed-match test, and two semantic-storage tests that ordered string values go, since ordering strings is not expressible in the union. An `In` over no values admits nothing: every compiler renders it as false, so EventMemory's empty id lists (session, source, block kind) keep meaning "nothing", and its complement admits everything. Every other file in this commit changes node names, import paths and dispatch shape only, with the same output as before. The tests the rebuilt base adds use the closed union too, and the segment store's random context-read test models the new semantics: a condition on a property with no value is false and a negation is the complement, where the model had followed SQL's three-valued logic. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
A leaf whose value was of another type than its key declares matched nothing on every store, each compiler rendering it as a filter no record satisfies, so a caller who compared a key with the wrong type got an empty result and no sign of the mistake; and `score > 3` on a float key matched nothing, since the parser reads `3` as an int. A NaN or an infinity reached the backends too: it matched every held value on Qdrant's REST transport, a NaN failed on its gRPC transport, and either failed on Milvus. `bind_filter` replaces `validate_supported_filter` at every store's query and returns the filter the store compiles in place of the caller's. It still refuses an undeclared key, and in the filter it returns every value is of its key's declared type: an int for a float key becomes the float it equals, and a membership test of ints on a float key the disjunction of those floats' equalities, since a membership test holds ints or strs. Any other value of another type than its key declares, a bool for a number, and a float that is not finite raise PropertyTypeMismatchError, which `upsert` already raises for a record; the VectorStorePartition query documents it. The compilers' branches that rendered a mistyped leaf as no match are unreachable and go: the Qdrant store's no-match filter, the Milvus store's filtering of values by type, and the SQL columns compiler's false branches, keeping only an `In` over no values as false. None of the three needs the declared schema any more. The Qdrant and Milvus design documents describe the binding. The declared-schema contract replaces its no-match test with tests every store runs: a value of another type and a float that is not finite each raise from `query`, and an int compares with a float key as the float it equals, each filter and its complement. The first two fail with the check in `_validate_value` removed, the non-finite cases also with its finiteness clause removed, and the third fails on Qdrant, whose float index refuses an int match, with the int-to-float conversion removed from `_bound_value` or from the membership branch of `_bound_filter`. The Milvus and Qdrant modules' own no-match tests go, the Qdrant one with the strict-mode reason it stated. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A record could not hold an int for a float key: upsert refused it as a type mismatch, though a filter may compare a float key with an int. A float property that is not finite reached the backends instead, and each kept it its own way: the SQLite stores held a NaN as no value and an infinity as written, Qdrant's REST transport held either as no value, and Qdrant's gRPC transport and Milvus refused the upsert. `bind_record` replaces `validate_declared_properties` at every store's upsert and returns the record the store writes in place of the caller's: its keys are declared and every value is of its key's declared type, an int for a float key becoming the float it equals. Any other value of another type, a bool for a number, and a float that is not finite raise PropertyTypeMismatchError, by the same rule the bound filter follows. The in-memory test partition binds its records too, and the VectorStorePartition upsert and the error document the rule. The declared-schema contract checks the rule on every store: an int written to a float key is stored as the float it equals, read past the store through a new `stored_value` hook each store's test module supplies, and matched by a float filter, an int one, and a membership test of ints; and each mistyped or non-finite value raises from `upsert`. The per-store tests of refused values lose their int-for-float case. The first test fails with the int-for-float clause removed from `_validate_value`, and on Qdrant, which keeps an int payload as an int, with `bind_record` returning the caller's record; the second fails with the finiteness clause removed, and its bool cases with a bool counted as an int. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every backend holds an int in 64 signed bits, and each answered a wider one its own way. A record holding one failed to write on the SQLite stores, on Qdrant's gRPC transport, and on Milvus, and Qdrant's REST transport stored it as the largest int64, which an equality filter on that value then matched. A filter holding one failed on the SQLite stores and on Milvus, failed an equality on Qdrant, and answered an ordering on Qdrant with its bound clamped, so `>= 2**63` matched the largest int64. The binding both `bind_record` and `bind_filter` apply now refuses an int outside -2**63 to 2**63 - 1 for any key with PropertyTypeMismatchError, before anything is sent; the bounds are MIN_INT_VALUE and MAX_INT_VALUE beside the error. An int for a float key is held to the same range, so its float always exists. The declared-schema contract adds out-of-range ints to its mistyped records and filters, and a test that every store compares the int64 extremes with filters at those extremes. The mistyped cases fail with the range clause removed from `_validate_value`, and the extremes test with the range made exclusive. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The declared-schema contract gains a test that every store matches strings holding accented letters, an emoji, quotes, a backslash, and control characters, by equality and by membership. It fails on Milvus with `ensure_ascii=False` removed from `_expression_string_literal`, the UTF-8 literal [vector store scale-out 6/6] ports from MemMachine#1736's 1a7fffa. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
A filter leaf named its field by the client's spelling, and every
consumer re-derived what the name meant from the string:
`normalize_filter_field` and the mangling helpers told a user property
(`m.<key>`, `metadata.<key>`) from a system field (a bare name) in the
episode store, the semantic storages, short-term memory, LongTermMemory,
EventMemory and the segment store. A leaf's field is now a reference:
`SystemField(name)`, a field the consuming system defines, or
`Property(key, type)`, a property by key with its declared type where a
schema declares one. The parser resolves the syntax once, and nothing
below it parses it; only LongTermMemory's message for an unknown name
still spells it, as the hint it gave the client. The helpers that
re-derived the kind go.
Each consumer maps the references it defines and refuses any other.
The episode store, the semantic storages and short-term memory map a
SystemField to their columns or attributes and a Property to their
metadata, as they mapped the spelled names. The declarative backend
maps a SystemField to the filterable property of its name and a
Property to its stored `metadata.<key>` property, the layout it keeps.
The vector stores and graph stores compile a Property and refuse a
SystemField with ValueError, `bind_filter` before anything is sent;
DeclarativeMemory refuses one too.
LongTermMemory's event backend now resolves its system fields: each
becomes the Property of its stored key (`producer_id` the property
`_producer_id`), the timestamp stays `SystemField("timestamp")`, and an
unknown name raises ValueError as before. EventMemory takes only
properties and the timestamp, sending the vector store the timestamp as
its `_timestamp` property, and the segment store maps the timestamp to
its column and a property to its JSON key. Both raise ValueError for any
other system field, where each mapped a bare name `x` to the property
`_x`. That replaces the ruling that EventMemory takes the client's
names and the segment store resolves them.
Where output changes: filter_parser.py (leaves carry references, and
`to_property_filter` keys an equality on a property by its key, where
it kept the spelled `m.<key>`); event_memory.py and
sqlalchemy_segment_store.py (a system field other than the timestamp
raises); common/vector_store/utils.py, neo4j_vector_graph_store.py,
nebula_graph_vector_graph_store.py and declarative_memory.py (a system
field raises). long_term_memory.py moves the resolution the two event
memory modules did, so the stores see what they saw. Every other source
file changes names, dispatch and docstrings only, with the same output
for the same filter text. The public filter syntax is unchanged.
The in-memory fakes follow their stores: the vector store fake's
`evaluate_filter` reads a Property from the record's properties and a
system field from a mapping its caller passes, which the segment store
fake fills with the segment's timestamp. Tests are added for the
parser's resolution, the refusals and LongTermMemory's mapping; the
segment store's and EventMemory's tests that a context field matches
nothing now expect the refusal, and the tests of the removed helpers
go. Node constructions in the remaining tests follow in the next
commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mechanical: every test that built a leaf from a spelled field name
builds it from the reference the parser makes of that name, so every
test asserts what it asserted. `m.<key>` and `metadata.<key>` become
`Property(key)`, and a bare name the parser reads `SystemField(name)`.
A store's own fields become `Property(key)` where the store filters on
properties, which is every vector store, graph store and declarative
memory test. The EventMemory tests name LongTermMemory's stored keys,
`Property("_episode_uid")` for `episode_uid`, now that LongTermMemory
resolves them, and the timestamp as `SystemField("timestamp")`.
The helpers that took a spelled name take a reference, with the same
results: `nodes.comparison`; `random_filter` and `random_properties`,
over keys; the SQL filter tests' field resolvers; the segment store
test's model of a filter; the Qdrant test's model, by `property_key`;
and the map_filter_fields tests, which swap the two kinds where they
upper-cased a name. Test docstrings that described the spelled names
describe the references. No test changes what it checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`_validate_value` refused a float that is not finite and an int outside 64 signed bits with the type mismatch's message, so a NaN for a float key read "declared as float, got float nan". PropertyTypeMismatchError stays the one error, and takes what else is wrong with a value of an admitted type: its message then reads "got float nan, which is not finite" or "got int 9223372036854775808, which is outside the 64-bit signed range". A value of another type keeps the mismatch's message. Where output changes: the two messages, in common/vector_store/utils.py and data_types.py. A new test module checks each message from `bind_record` and `bind_filter`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at and a datetime equal only themselves A membership test holds ints or strs, which every store matches exactly. `In` now refuses a value of any other type, a float or a datetime included, where it refused only a bool, and a field referenced as a float or datetime property. `bind_filter` refuses a membership test on a key declared float or datetime with ValueError, before its values are checked, and no longer turns ints on a float key into a disjunction of float equalities. VectorStorePartition's query contract states it. Ordering is unchanged: ints, floats and datetimes order, and strings and bools do not. Equality on floats and datetimes stays, every backend comparing them exactly, and tests now check that a float and a datetime equal only themselves on the declared-schema contract (the SQLite stores, Qdrant and Milvus), the typed-JSON compile on SQLite and PostgreSQL, the segment store and the Neo4j graph store. The segment store's floats stay below 1e16, which PostgreSQL's JSONB reads back as an int (MemMachine#1797). Where output changes: filter_expression.py (`In` construction) and common/vector_store/utils.py (`bind_filter` on a float or datetime key); vector_store.py changes its docstring only. The declared-schema contract's int-for-float membership cases go, its mistyped membership test on a float key moves to a test of the refusal, and the parser tests check the `In` refusals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parser read `3` as an int whatever it was compared with, so a
condition on a float field reached the stores holding an int, and each
store chose what to do with it. `parse_filter` now takes the static
types of the target system's fields and types each literal compared
with one: an int for a float field is the float it equals, and any other
literal of another type is a FilterParseError, which the API answers
with 422 as it does every filter error. A string for a datetime field is
refused with the hint that a datetime is written `date('...')`, a float
for an int field and a bool for a number are refused, and so is a
membership test on a float or datetime field. A property (`m.<key>`)
stays dynamically typed, keeping its literal's own type, as does a name
no target types.
MemMachine parses a request's filter once per memory it reaches, before
any search starts: the episodic search by the event backend's fields
(`EVENT_BACKEND_FILTER_FIELD_TYPES`, its stored system fields by name
and EventMemory's datetime `timestamp`), the episode listing and count
by the episode storage's (`EPISODE_STORAGE_FILTER_FIELD_TYPES`), and
semantic memory by the semantic storage's
(`SEMANTIC_STORAGE_FILTER_FIELD_TYPES`).
A property reference carries its type wherever one is known:
LongTermMemory resolves a system field to `Property("_<name>", <its
declared type>)`, EventMemory sends the vector store its timestamp as
`Property("_timestamp", datetime)`, and `bind_filter` sets each
property's declared type. EventMemory picks the part of the filter the
vector store gets by each property's key, so a typed reference still
reaches it, and maps the filter's fields before it embeds the query, so
a system field it does not define raises before any work.
Where output changes: filter_parser.py, main/memmachine.py,
long_term_memory.py, event_memory.py and common/vector_store/utils.py.
episode_storage.py and storage_base.py add the field types, and the
package inits export them. Tests are added for the typing and its
refusals, the per-memory parse, a typed reference reaching the vector
store and the refusal before the embedding; the expectations of the
LongTermMemory and EventMemory routing tests carry the types. The other
tests' calls of `parse_filter` follow in the next commit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mechanical: every test that parses a filter passes `parse_filter` the field types of the memory it targets, now that the parser requires them: the semantic tests the semantic storage's, the episode storage test the episode storage's, and the short-term memory tests the event backend's, which MemMachine parses an episodic search by. The parser's own tests, the compile tests and the segment store's pass no types. No test's filters type differently, so every test asserts what it did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A property no schema declares holds whatever type was written, and the stores compared a number with it only when the types matched: the segment store's typed JSON refused an int compared with a held float, so `m.score > 3` dropped a segment holding 3.5 that the vector store had admitted, and the episode store's and semantic storages' raw JSON cast the held value to the literal's type, truncating 3.5 to 3 on SQLite and failing on PostgreSQL. An int and a float are now one numeric type in every store holding dynamic properties. Both JSON encodings in sql_filter_util compare numbers as SQL NUMERIC, exactly: the held value is cast from its JSON digits, and the literal from its shortest round-trip digits, which order like the float itself, so nothing beyond 2**53 rounds. The typed JSON admits a value tagged int or float, the type check guarding the cast in a CASE. Short-term memory orders numbers natively where it compared floats, and the in-memory vector store partition, which the segment store fake shares, counts an int and a float as one type. A declared key holds and is compared with its declared type, so its results are unchanged. Where output changes: common/filter/sql_filter_util.py and short_term_memory.py; the in-memory partition is a test fake. Tests check numbers by value and beyond 53 bits on the compile (SQLite and PostgreSQL), the segment store, the episode store, the semantic storages and short-term memory, with float and datetime equality on the episode store and float equality on the semantic storages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EventMemory wrote an event's properties to the segment store as given and let the vector store bind them at its upsert, after the segments were written: an int for a float key became a float in the vector record and stayed an int on the segment, and a value of another type failed the upsert with its segments already stored. `_validate_events` now binds each property the vector store declares, by the rules `bind_record` applies, before anything is written: an int for a float key is the float it equals in both stores, and any other mismatch raises PropertyTypeMismatchError with nothing stored. A key the vector store does not declare keeps its value. `bind_properties` is `bind_record`'s binding of values, without its refusal of undeclared keys; `bind_record` calls it. Where output changes: event_memory.py; common/vector_store/utils.py moves code without changing its output. Tests check both stores hold the bound value and that a mismatch writes nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Neo4j graph store and the Neo4j semantic storage rendered `Not` as Cypher's NOT, which is null over a comparison on a missing property, so a negated condition dropped the nodes that lacked the property, where every other store keeps them. Both now render `NOT coalesce((<operand>), false)`: the operand is false where it was null, and the negation is its complement, as on the SQL stores, Qdrant and Milvus. The Nebula graph store is unchanged. Where output changes: neo4j_vector_graph_store.py and neo4j_semantic_storage.py. The graph store's test checks each node and its negation against nodes missing the compared property, and the semantic storages' test, which every storage runs, does the same for a feature without the metadata key. Both fail on Neo4j with the change reverted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d by The episode store's and the semantic storages' resolvers each listed their system fields' names, which EPISODE_STORAGE_FILTER_FIELD_TYPES and SEMANTIC_STORAGE_FILTER_FIELD_TYPES list again for the parser, so a field could drift to filterable but untyped at parse, or the reverse. The type maps are now the one list: a resolver refuses a name the map does not hold, and keeps only what the store adds, the column of each name held under another (`id` is `uid`, `category` is `semantic_category_id`) and the episode store's UUID encoding of `uid`. The Neo4j semantic storage's own list of known fields goes the same way; it still takes `created_at_ts` and `updated_at_ts`, the names of its stored timestamps, which no other storage does. The episode store still matches a system field's name case-insensitively and the parser types it by its exact name; this commit leaves both rules as they are. Where output changes: none. episode_sqlalchemy_store.py, sqlalchemy_pgvector_semantic.py, vector_store_semantic_storage.py and neo4j_semantic_storage.py resolve the same names to the same columns. Tests check that a filter resolves every name in each map, and the episode store's by a name of another case; Neo4j skips the semantic one, holding some of the aliases under no node property. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The episode store matched a system field's name case-insensitively, while the parser types a field by its exact name and every other filter target matches names exactly. It now matches them exactly too, by one rule everywhere, so a filter writes a field one way in every example and document. API change: a system field name in another case (`Producer_ID`, `SESSION`) in the filter of an episode listing or count is now refused, answered with 422 and "Unknown filter field", where it matched its lowercase field. No API deletes episodes by a client's filter, and the server's own deletion filter names `session_key` exactly. No document, OpenAPI description or example shows a field name in another case. Where output changes: episode_sqlalchemy_store.py. The resolution test drops its mixed-case names, and a test checks the listing and the count refuse them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Nov 3, 2026
Two versions of one record in one batch are ambiguous, and the stores disagreed on them: the SQLite store and Qdrant kept the last, the sqlite-vec store failed partway with sqlite3's UNIQUE constraint error, and Milvus refused the batch with its own error. Every store now raises ValueError naming the UUID before anything is sent to the backend, from one helper beside the other upsert checks, at a cost of one pass over the batch. The VectorStorePartition.upsert docstring says so, and the registry-backed `_upsert` hook may rely on distinct UUIDs. The declared-schema contract upserts a batch repeating a UUID after another record, expects the error, and finds nothing stored. Without the check it fails on every store: the SQLite store and Qdrant, over REST and gRPC, raise nothing, the sqlite-vec store raises sqlite3's OperationalError, and Milvus 2.6.24 raises its MilvusException. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A datetime written to Qdrant keeps its UTC offset only to the minute, over REST and gRPC alike: on 1.19.1, one written at -00:44:30 reads back at -00:44. The store wrote a datetime property at its own offset, so one at an offset with a seconds component, as local mean time had until 1972, was stored up to 59 seconds off its instant, and a filter on that instant missed it. The store now converts the datetime to UTC before writing it, which applies the whole offset to the instant, so nothing is left for Qdrant to cut. The store's design note says so. The read-back test writes a datetime at +05:30 and finds the payload holding the written instant in UTC, over REST and gRPC. Without the change, the payload keeps +05:30. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A partition's properties exist for filters, which compare a datetime by its instant, and a search answers with record UUIDs and scores, so no caller reads a stored offset back. The VectorStorePartition docstring now says a store keeps a datetime property's instant, taking a naive datetime as UTC, and may drop its UTC offset, and that a filter compares datetime values by their instant, for equality and ordering alike. The declared-schema contract checks both on every store. A datetime written at +05:30 matches the same instant at -08:00 under =, <=, and >=, a later instant at an earlier wall-clock time under <, and an earlier instant at a later wall-clock time under >. A datetime written at -00:44:30 matches its instant under =, whether the filter gives it at that offset or in UTC, and matches neither < nor >. On Qdrant, over REST and gRPC, that test fails without the store's write in UTC. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
The filter language becomes a closed union:
Equals,Ordering,In,IsNull, n-aryAnd/Or,Not(common/filter/filter_expression.py); every compiler is an exhaustivematch.!=and<>parse asNot(Equals),NOT INasNot(In),IS NOT NULLasNot(IsNull), so a negation means one thing. An ordering comparison takes a number or a date:field > 'string'andfield > true, which parsed before, are now rejected by the parser.A leaf on a field holding no value is false,
IsNullon it is true, andNotis the complement over the record set on every vector store and the segment store. The SQL compilers render it asNOT COALESCE(x, FALSE):sql_filter_util.pycompiled~inner, three-valued, and a negated filter silently dropped rows holding no value on every SQL-backed store. Milvus already pushesNotto the leaves by De Morgan, since [vector store scale-out 5/6] (#1736), as its ownnotexcludes entities lacking the field, and Qdrant'smust_notalready admits them. The Neo4j graph store and the Neo4j semantic storage renderNotasNOT coalesce((x), false), the complement, and the NebulaGraph graph store keeps itsNOTover a comparison three-valued (#1918), since its tests need a NebulaGraph Enterprise 5.0 server. A filter is bound to the store's declared schema before the store compiles it (bind_filter), and a record before the store writes it (bind_record): an int for a float key becomes the float it equals, and a float for an int key, a bool for a number, a non-finite value, or an int outside 64 signed bits raisesPropertyTypeMismatchError, whose message says which, so no compiler meets a value of another type than its key declares. EventMemory binds an event's declared properties in_validate_events, before anything is written, so the segment store and the vector store hold the same value. AnInover no values admits nothing: every compiler renders it as false, so EventMemory's empty id lists keep meaning "nothing", and its complement admits everything.A filter's fields are resolved when it is parsed, so nothing below the parser reads how a client spelled a field. Each leaf names a
SystemField(name)or aProperty(key, type)(filter_expression.py). The parser turnsm.<key>andmetadata.<key>into a property and any other name into a system field, andnormalize_filter_field,mangle_user_metadata_key,demangle_user_metadata_key,is_user_metadata_keyand theUSER_METADATA_*prefixes go. Each memory maps its own system fields: the event backend's to its stored keys, and the episode store's, the semantic storages' and short-term memory's to their columns and attributes. EventMemory takes onlySystemField("timestamp")and properties, the segment store maps the timestamp to its column and a property to its JSON key, and the vector stores and graph stores refuse a system field.A literal is typed by its field when the filter is parsed.
parse_filtertakes the target memory's system field types (EVENT_BACKEND_FILTER_FIELD_TYPES,EPISODE_STORAGE_FILTER_FIELD_TYPES,SEMANTIC_STORAGE_FILTER_FIELD_TYPES), and each store's resolver takes its field names from the same map. An int for a float field parses as the float it equals. A string for a datetime field (a datetime is writtendate('...')), a float for an int field, a bool for a number, or a number for a string field is a parse error, which the API answers with 422. A system field's name matches exactly: a mixed-case name such asProducer_ID, which the episode store matched before, is an unknown field. A user property's type is dynamic, and its numbers compare by value, an int and a float as one numeric type, exactly beyond 2⁵³ on SQLite and PostgreSQL.An
Intakes no float and no datetime on either side: a membership test of floats, datetimes or bools, or on a float or datetime field, is refused, where an int test on a float key was a disjunction of float equalities. An ordering comparison (<,<=,>,>=) takes an int, a float or a datetime. An equality on a float or a datetime is exact on every store: Qdrant compiles it as a closedRangeorDatetimeRange, since itsMatchValuetakes no float.A vector store refuses an upsert batch that names a record UUID twice, with ValueError before anything is sent, where the stores disagreed: the SQLite store and Qdrant kept the last version, the sqlite-vec store failed partway with sqlite3's UNIQUE constraint error, and Milvus refused the batch with its own error. A store keeps a datetime property's instant, taking a naive datetime as UTC, and may drop its UTC offset; a filter compares datetimes by their instant (the
VectorStorePartitiondocstring). Qdrant now writes a datetime in UTC: it keeps a written datetime's offset only to the minute, so one at an offset with a seconds component was stored up to 59 seconds off its instant, and an equality on that instant missed it. These come from [vector store write validation] (#1788), folded in here; its non-finite check is not, sincebind_recordalready refuses a non-finite value at every store's upsert.Last in the stack, because the store work is the priority and this change touches many files mechanically; the slices under it are written against the nodes
mainhas today. The complement law is checked over thirteen filters indeclared_schema_contract.py, across the column, JSON and properties encodings of the SQL stores.Adaptation for
main: the routing of [user properties 1/2] is rewritten on the closed union (And.operands;EqualsforComparisonwith=), and itsextract_filtermoves tofilter_expression.py, a match over the n-ary nodes exported fromcommon.filter, with its tests and random filters on the closed union; the speedkick-era commit 8ca5f06 was written on #1597's tree, and its rewrites of #1597's_system_predicates,_lift_typed_filtersand their tests have nothing to apply to onmain. The event memory test spells the fieldcontext.producer,main's name for it. The tests the rebuilt base adds use the closed union too, and the segment store's random context-read test models the new semantics: a condition on a property with no value is false and a negation is the complement, where the model followed SQL's three-valued logic. Through3176c1a92, the same 60 files as the origin; the commits after it are new here, with no origin counterpart.Position: directly on
feat/horizontal-scaling, which has [declared schema 1/2] (#1628) merged, in parallel with the SQLite fixes; the fixes' tests, which the origin also converts to the closed union, are not in this branch.Fixes #1780, #1781, #1783, and #1784.
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 16 commits,
2fd7f67f9,197757ec7,b0f862135,9a3efe165,3176c1a92,38bfc9e58,79cf89dc4,e39f219a8,2cc7d0fa9,602386499,3a0e53fd9,04c252b9d,4c0e9cbec,c093a94df,f09a4f7be,afac73b12, directly onfeat/horizontal-scaling.Verification
At this PR's head
2486d2d73, on 2026-11-03, its tree isafac73b12's with three commits from #1788 on top: an upsert batch naming a record UUID twice refused, Qdrant's datetimes written in UTC, and the datetime property contract stated. #1788's non-finite check is dropped: every store'supsert(the registry-backed base for Qdrant and Milvus, the SQLite store and the sqlite-vec store) binds each record withbind_recordfirst, whose_validate_valuerefuses a non-finite float, anddeclared_schema_contract.pychecks that on every store. The new contract tests run on every store: a batch repeating a UUID raises and stores nothing, and fails without the check on all five variants (the SQLite store and Qdrant over REST and gRPC raise nothing, the sqlite-vec store raises sqlite3's OperationalError, Milvus 2.6.24 its MilvusException); a datetime written at +05:30 compares by its instant at other offsets; a datetime written at -00:44:30 equals its instant, given at that offset or in UTC, and fails on Qdrant over REST and gRPC without the write in UTC. Qdrant's read-back test finds a datetime written at +05:30 held in UTC. Checks at the head:ruff checkandruff format --checkclean;ty checkclean as CI runs it;uv lock --checkclean. The unit tests pass, 2596 tests (8 skipped), and the vector store integration tests pass against containers the tests start (Qdrant 1.19.1, Milvus 2.6.24): Qdrant 356, Milvus 149, and the partition registry 56.Before #1788's commits: At this PR's head
afac73b12, on 2026-11-03, its tree is3176c1a92's with eleven commits on top: field references resolved at parse, the tests' nodes built from them, the refused-value wording, theInand equality rules, literals typed at parse, the tests' field types, numbers compared by value, events bound before any write, Neo4j's complement, each store's field names taken from its field types, and exact field names. Checks at the head:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass at the head, 2590 tests (8 skipped). Atc093a94df, the ninth of them, the unit tests pass, 2552 tests (8 skipped), and the integration tests pass against containers the tests start: Qdrant 348; the SQL filter, segment store and episode store tests on PostgreSQL 216; Neo4j 27 (graph store and declarative memory); semantic storage 1044 (pgvector, Neo4j and in-memory, 3 skipped by their Neo4j-specific skips, the two LLM-backed modules not run); and Milvus 146. The two commits after it were checked on what they touch. Atf09a4f7be: the episode store, semantic, main and filter unit tests (790 passed, 2 skipped), and the episode store and semantic integration tests (1145 passed, 15 skipped by Neo4j-specific skips). Atafac73b12: the episode store, main and server unit tests (453), and the episode store's integration tests on PostgreSQL (91). The NebulaGraph tests did not run, since they need an external NebulaGraph Enterprise 5.0 server.Before the filter-reference commits: At this PR's head
3176c1a92, on 2026-11-02, rebased ontofeat/horizontal-scalingat0fa52b38b, where [sqlite write transactions 1] (#1844) is merged since690116e37. Its own lines follow the base: the SQLite stores importFilterExprfrom the filter package beside #1844'svalidate_engine_connections. Its commits are re-signed, with their messages and authors unchanged. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 2479 tests (8 skipped). The integration tests did not run here.Before the rebase onto #1844's merge: At this PR's head
b566a2c15, on 2026-10-30, restacked directly onfeat/horizontal-scalingat690116e37, where [declared schema 1/2] (#1628) is merged as5c9d3ae99, with #1831 before it and #1858 after it; [sqlite write transactions 1] (#1844) is no longer beneath it, since this PR uses none of #1844's code. Its own lines follow the base: the declared-schema contract keeps both #1628's re-upsert test and this PR's own tests where they meet, and keeps_presence_records, which #1628's last commit removed and this PR's tests call;with_has_set_ids, which #1831 made public, returnsFilterExpr | Noneas this PR has it; and #1831's semantic storage test builds its membership filter with a tuple, as this PR'sInholds. Its commits are re-signed, with their messages and authors unchanged. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 2470 tests (8 skipped). The integration tests did not run here.Before the restack onto
feat/horizontal-scalingafter #1628's merge: At this PR's head526b3d52e, on 2026-10-28, restacked on [sqlite write transactions 1] (#1844) at4f5d4430c, on #1628'sdf716e47b: its tree is the previous headcb0da1545's with #1628's two changes since274264523beneath it (is_valid_identifier's docstring, and the SQLite store's records table built withoutextend_existing), and its own lines are unchanged. Its commits are re-signed, with their messages and authors unchanged. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean.Before the restack onto #1628's
df716e47b: At this PR's headcb0da1545, on 2026-10-27, rebuilt on [sqlite write transactions 1] (#1844) at0bcbf8b3a, with #1628's second round of review commits (the identifier rule indata_types.py, which imports nothing from the package, and the interface's Raises sections sorted by name) beneath it. Its own lines follow them:utils.pyimports the errors,Record, and the int range fromdata_types.pyby name, sobind_recordannotates withRecorditself;MIN_INT_VALUEandMAX_INT_VALUEsit indata_types.pywith no import from the package; and the Raises entries this PR adds toupsertandquerytake their places by name. Its commits are re-signed, with their messages and authors unchanged. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 2446 tests (8 skipped). The integration tests did not run here.Before #1628's second round of review changes: At this PR's head
85d44a9a2, on 2026-10-27, rebuilt on [sqlite write transactions 1] (#1844) at69e292d8a, with #1628's four commits from Edwin's review (nosupported_filter_nodes, its checks inutils.py,sqlite_properties.py, and the strict-mode comment) beneath it. Its own lines follow them:bind_filterandbind_record, with their helpers, live inutils.pybeside the other validators and raise their errors through thedata_typesmodule, which importsutils.py;bind_filtertakes no supported nodes, since every store evaluates the whole filter language; andfilter_expression.pyand the filter package's exports dropfilter_nodes. The first two commits' messages namesqlite_properties.pyand no longer mention the node check. Its commits are re-signed, with their authors unchanged. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 2446 tests (8 skipped). The integration tests did not run here.Before #1628's review changes: At this PR's head
3a803816e, on 2026-10-27, moved onto [sqlite write transactions 1] (#1844) at497a89ef1, which now sits on #1628: its tree is the previous head08c6dc525's, so the checks and tests at that head stand. Its commits are re-signed, with their messages and authors unchanged.Before #1844 moved above this PR: At this PR's head
08c6dc525, on 2026-10-27, replayed onto the replayed #1628's135184cd6, after [vector store scale-out 6] (#1627) merged intofeat/horizontal-scalingas805ea2177, a squash whose tree is8c2351dbe's: its tree is the previous headac7c6eda7's with #1628's test-only commit beneath it, which gives the oversized Milvus upsert test's own store a longer request timeout. Its commits are re-signed, with their messages and authors unchanged. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The integration tests did not run here.Before the replay onto the merged #1627: At this PR's head
ac7c6eda7, on the rebuilt #1628, on 2026-10-26: beneath it are #1627's changes sincee38e8003e(each store's schema passed as oneVectorStoreSchema,store_schema; SQLite partitions named by an incarnation and created as the other stores create theirs; the shared SQL engine setup and write transactions incommon/sql.py; and every session's event memory served from the deployment's configuration, in one store) and #1844's SQLite write transactions. This PR's own lines follow them: the stores bind filters and records againststore_schema.indexed_properties; the two SQLite store tests #1627 added with aComparisonfilter useEquals; and the Qdrant store drops theMappingandPropertyTypeimports with the compiler's schema argument. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 2446 tests (8 skipped). The integration tests did not run here.Before the rebuild onto #1627's
8c2351dbe: At this PR's head8c76235b1, on 2026-10-20, its tree is the previous head6d67208b1's with #1627's accuracy fixes, in text only: the partition registry and purge design documents, the database manager's comments and docstrings, the SQLite store's module docstring, the partition registry configuration's descriptions, and other docstrings that still described collections. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1355 tests (8 skipped). No integration tests ran this time, since only comments, docstrings, descriptions, and documentation changed.Before #1627's accuracy fixes: At this PR's head
6d67208b1, on 2026-10-19, its tree is the previous head5ddf3a290's with #1627's second review's fixes: the Qdrant and Milvus tests that partitions share the store's collection now write through two partitions and read the store's one native collection; the store tests name their partition fixturepartition; and docstrings, comments, and the configuration and upgrade documentation are corrected. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1355 tests (8 skipped), and the vector store and segment store integration tests (the registry on PostgreSQL and SQLite, both SQLite stores, Qdrant, Milvus, and the SQLAlchemy segment store) pass, 642 tests.Before #1627's second review's fixes: At this PR's head
5ddf3a290, on 2026-10-19, its tree is the previous head5a9d2d9eb's with #1627's review fixes: the database manager answers a repeated (backend, vector store name) with the store it built, without comparing parameters, and keeps a backend's shared SQLite engine until it closes, even when a store's build fails; its test that one SQLite backend's stores share the engine can now fail; the partition registry's parameters check the vector store name with the sharedvalidate_vector_store_name; and semantic memory's local name for the backend key isbackend. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1355 tests (8 skipped).Before #1627's review fixes: At this PR's head
5a9d2d9eb, on 2026-10-19, moved onto [declared schema 1/2] (#1628): its tree is the previous head18ef07b12's without [search results] (#1663) beneath it. This PR's own lines keepSimilarityMetricbeside its filter imports. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1355 tests (8 skipped), and the vector store and segment store integration tests (the registry on PostgreSQL and SQLite, both SQLite stores, Qdrant, Milvus, and the SQLAlchemy segment store) pass, 642 tests.Before the move off [search results] (#1663): At this PR's head
18ef07b12, on 2026-10-19, its tree is the previous head3ae24ee56's with design/vector_store_partition_registry.md listing the lookup as "open", the name it has from #1627 until #1625, where it said "get". Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. Outside that document the tree is the previous head's, so the test runs below stand.Before the partition registry document's wording fix: At this PR's head
3ae24ee56, on 2026-10-19, its tree is the previous head2af6ac444's with #1824 beneath it, which drops the purge queues' unread logical key, and #1627 dropping the same key from the partition registry's tombstones and removing the collection registry design document its partition registry document replaces. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1333 tests (8 skipped).Before the rebase onto #1824's merge: At this PR's head
2af6ac444, on 2026-10-16, its tree is the previous head291a4a103's with the configuration's check of a providedtombstone_retention_secondsnamed_validate_tombstone_retention, where it was_require_retention_floor. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1333 tests (8 skipped).Before the retention check's rename: At this PR's head
291a4a103, on 2026-10-16, its tree is the previous head85a4b756a's with every vector store check of a value that raises namedvalidate_, asvalidate_partition_keyalready was:validate_declared_types,validate_query_vector,validate_score_threshold,validate_limit, andvalidate_dimensionsfrom the base, and the PRs' own checks in their turn. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1333 tests (8 skipped).Before the raising checks took the
validate_prefix: At this PR's head85a4b756a, on 2026-10-16, its tree is the previous heada7950f283's with #1627's changes: theVectorStoreinterface dropsvector_store_name,vector_dimensions,similarity_metric, andindexed_properties, and the partition handle dropspartition_key; a partition key may be any string of up to 255 bytes without NUL, and the SQLite stores name a partition's tables and index file by a SHA-256 digest of it; and the checks that return a bool areis_valid_identifierandis_valid_filter. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1333 tests (8 skipped), and the vector store integration tests (the registry on PostgreSQL and SQLite, both SQLite stores, Qdrant, and Milvus) pass, 512 tests.Before the store's properties left its interface: At this PR's head
a7950f283, on 2026-10-15: its tree is the previous head150acbc8e's without #1627's per-partition schema record (PartitionSchema, the registry's and the SQLite stores'schemacolumn, andVectorStorePartitionSchemaMismatchError), and withrequire_partition_keynamedvalidate_partition_key;ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. The unit tests pass, 1327 tests.Before the per-partition schema record went: At this PR's head
150acbc8e, on 2026-10-15: its tree is the previous head2b82dbde4's with the partition lookup namedopen_partitionand the segment store'sclose_partitionkept, since #1625 renames the one and removes the other;ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean.Before the lookup's rename moved into #1625: At this PR's head
2b82dbde4, on 2026-10-15: its tree is the previous head580538da1's without the segment store'sclose_partition;ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean.Before #1627's first commit took the removal of
close_partition: At this PR's head580538da1, on 2026-10-15, restacked on #1627's rewritten history: its tree is the previous headf61920cf6's;ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean.Before #1627's history was rewritten: At this PR's head
f61920cf6, on 2026-10-15, rebased ontofeat/horizontal-scalingatcf7ae1579, which carriesmainthrough #1774:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean; the MemMachine mock and session deletion tests pass, 48 tests (3 skipped).Before the rebase onto
cf7ae1579: At this PR's head04943f87d, on 2026-10-15, rebased ontofeat/horizontal-scalingwith #1702 and #1670 merged (101182c18):ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean.Before the rebase onto #1670's merge: At this PR's head
ad48f4407, on 2026-10-13, rebuilt on #1702's6631b5c77:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean; the episodic memory and filter tests pass, 503 tests (1 skipped). Each of seven deliberately broken versions ofextract_filterfails these tests, and the table of expected parts alone covers every line and branch but the closed match's unreachable fall-through.Before the rebuild on #1702's filter routing: At this PR's head
13d104245, on 2026-10-09, onfeat/horizontal-scalingwith #1813 merged:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean; the Qdrant store's, the registry-backed store's, and the database manager's tests, unit and integration, pass, 386 tests, against Qdrant 1.19.1 defaulting new collections to strict mode and Milvus 2.6.24 in containers.Before the rebase onto #1813's merge: At this PR's head
df21dcdda, on 2026-10-09:ruff checkandruff format --checkclean;ty checkclean as CI runs it (uv run --frozen --all-extras ty check --project packages/server);uv lock --checkclean. Its tree is the previous heade9b2fc25c's: the server suite without integration tests last passed atd491433ce, on 2026-10-08, 2235 tests, and the integration tests of the vector stores, the resource manager, episodic memory, and semantic storage atd0ba0b451, on 2026-10-08, 840 tests, against PostgreSQL 16, Neo4j, Qdrant 1.19.1, and Milvus 2.6.24 in containers. The SQLite stores' tests last passed at372d94164, on 2026-10-09, 270 tests, after the SQLite offset column was dropped.🤖 Generated with Claude Code
https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn