Repository navigation
[sqlite store fixes 4/7] Serialize a partition's writes so the engine sees them in order (port of #1607) - #1673
Conversation
d25a9e9 to
b80ff04
Compare
b80ff04 to
cfbfcca
Compare
cfbfcca to
ee6fbfd
Compare
edwinyyyu
left a comment
There was a problem hiding this comment.
Stale vocabulary carried from the speedkick version into main (line comments). On main after #1627 a store is one collection and the lock this PR adds is per partition incarnation (_write_locks: dict[UUID, asyncio.Lock], _write_lock_for(incarnation)); handles come from get_partition, not open_collection. #1676 fixes the docstring at 1228 in passing; the class and test names and the TestConcurrentWriteOrdering docstring remain.
| assert set(matched) == {record.uuid for record in records} | ||
|
|
||
|
|
||
| class TestOneLockPerCollection: |
There was a problem hiding this comment.
TestOneLockPerCollection / test_two_handles_on_one_collection_are_serialized / "per open_collection call": on main it is one lock per partition incarnation and get_partition.
| return applied == 2 | ||
|
|
||
|
|
||
| class TestConcurrentWriteOrdering: |
There was a problem hiding this comment.
"a collection's writes": a partition's, on main (the store is the collection).
ee6fbfd to
7bcb477
Compare
c5fce82 to
3242359
Compare
3242359 to
d5f351d
Compare
093d4b7 to
7c6bc19
Compare
…emMachine#1588) * Atomically swap vector search engine index files on save SQLiteVectorStore persists each collection's index by calling the search engine's save(), which wrote directly to the final path. A crash mid-write left a truncated/corrupt file. Because index_saved=True makes the on-disk index a durable contract (missing/corrupt is a hard IndexLoadError, not a silent empty rebuild), an interrupted save could render a collection unrecoverable. Write the index to a sibling temp file and swap it into place with os.replace (atomic on POSIX and Windows on the same filesystem), so a reader sees either the old or new index, never a partial write; a failed save leaves the previous index intact. Leftover temp files are cleared on load so a crash does not leak them across restarts. Implemented in the engines (shared index_persistence helper) rather than in SQLiteVectorStore/SQLiteVectorStoreCollection, since the index save location and number of files written differ across engine implementations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make the index swap durable, not only atomic The swap protects a reader from a torn index, but the vector store also trims its pending-operation log once `save` returns -- and that log is the only other copy of those vectors, since the records table stores no vector column. So the swap reaching disk is load-bearing rather than a bonus: - fsync the parent directory after the replace, since POSIX `rename(2)` leaves the new directory entry in the page cache. Best-effort and ignored on failure, matching SQLite's `unixSync`; a no-op on Windows, which has no equivalent operation. - stop swallowing a failed fsync of the temp file. SQLite draws the same line -- a file fsync failure raises SQLITE_IOERR_FSYNC while a directory fsync failure is ignored -- and `EIO` means the writeback already failed and the dirty pages were dropped, which is exactly when the save must not be reported as committed. The existing cleanup then leaves the previous index in place with the log untrimmed, so the next save retries. - use F_FULLFSYNC on macOS, where plain `fsync` leaves the data in the drive's volatile write cache, falling back when a filesystem refuses it. State the resulting obligation on `VectorSearchEngine.save` itself, since that is what the store now relies on: replace atomically, then make the replacement as durable as the platform allows. An engine whose backend already implements the whole protocol can delegate to it and skip these helpers; the rest use `atomic_index_write`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the engine own index durability, not the vector store The pending log holds the only durable copy of a vector between checkpoints -- the records table has no vector column -- so trimming it is safe only at an instant when the index provably holds those vectors. The temp-write + rename protocol this PR shipped could not provide that instant. A rename changes a directory entry, and Windows exposes no way to flush one: os.fsync is _commit, which is FlushFileBuffers, which is for file data, and you cannot open a directory to fsync it. The decisive evidence is SQLite's own -- it threads a directory-sync flag through every commit-relevant directory operation, honors it in unixDelete, and declares it /* Not used on win32 */ in winDelete. So os.replace could return, _save_collection_index could commit its trim durably behind it, and a power cut could still roll the rename back: records forward, index back, no copy of the difference left. MOVEFILE_WRITE_THROUGH is not a fix; its documented guarantee covers copy-and-delete (cross-volume) moves, not same-volume renames. Take SQLite's answer, which was not to harden the directory operation but to stop using one as a commit point (PERSIST commits by zeroing a header, TRUNCATE by truncating, WAL by appending frames). A base path now expands into two index slots plus a generation record each, created once and thereafter only overwritten. A checkpoint writes the index over the inactive slot and flushes it, then writes that slot's generation record and flushes that. The record is the commit, and it is a write into a file that already exists. It holds the generation and its bitwise complement, so a torn write reads as absent rather than as some other generation -- all or nothing without needing single-sector atomicity from the hardware. load takes the highest believable generation, and deliberately does not fall back to the older slot when the published index will not parse: the log was trimmed against the newer one, so the older is stale by exactly the ops that can no longer be replayed. Both backends already write straight to the path they are given, which is what this protocol wants -- verified that repeated saves preserve the inode and leave no stray files -- so no engine gains a temp file, a buffer, or a rename. Durability is entirely the engine's, including which artifact is live. The store keeps no slot pointer, manifest, or generation, so no schema change and no migration: what remains is one rule, never trim past what save says is durable, and _save_collection_index already had that order. index_path becomes index_base_path since it no longer names a file, and discarding a collection asks the engine layer which files that covers. BREAKING CHANGE: an index written by the previous protocol is not published under the new one, so a collection with index_saved=True raises IndexLoadError until its index directory is cleared and the records re-ingested. Anomaly tests walk every crash point in the publish sequence by constructing the on-disk state each would leave, plus one that pins the ordering itself (a failed index write must publish nothing) since state-based tests cannot observe it. Verified against three deliberate breaks -- dropping the complement check, writing the record first, and reusing one slot instead of alternating -- each caught. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Publish the index atomically, and stop promising durability The two-slot generation-record protocol bought a guarantee we have decided not to make: that a save survives a power failure. Every engine would have to implement and maintain that protocol, and the failure it buys out is bounded -- search recall for the records applied since the last checkpoint, repaired by re-ingesting them. The direction that actually costs, a published index that will not parse, is closed by the atomic swap on its own. So this returns to the temp-file-plus-rename publication and spends the difference on stating the contract instead of strengthening it: `save` publishes atomically, never durably; the store trims the pending log behind a publication a power failure can revert; a record whose vector is lost that way still resolves by uuid, is absent from search until it is upserted again, and nothing here detects the gap for the caller. Reverts the durability and engine-owned-publication commits, keeps the atomic swap, and adds a store-level test that reconstructs a reverted publication deterministically -- restore the previous index bytes after the trim -- to pin the direction it fails in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Report a lost embedding as lost, not as a missing feature `update_feature` reads the stored embedding back when a caller updates a feature without supplying one, and that is the only place in the server that depends on the index still holding a vector. With publication now atomic rather than durable, a power failure can leave a feature whose row is intact and whose vector is not -- a state this path reported as "Vector record not found", which points the caller at the wrong thing and hides the repair. Split the two cases. A record that is genuinely absent keeps the old message; a record whose embedding the index no longer holds says so and names the fix, which is to pass a fresh embedding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let a failed fsync fail the save, and cut the essay around it `_flush_to_disk` wrapped its fsync in `contextlib.suppress(OSError)` and called itself best-effort. A failed fsync is exactly the evidence that the bytes are not safe to publish -- on Linux an EIO from fsync means writeback failed, reported once and then cleared -- so swallowing it and renaming anyway published a file we had positive evidence was bad. The safeguard cost something and, in the one case it existed for, guaranteed nothing. Nothing tested it either. Let it propagate. `atomic_index_write` already unlinks the temp and re-raises, so a failed flush now leaves the previously published index standing, which is the correct outcome. A test pins that. The fsync is not best-effort, and the docstring should not have said so: it rules out a class rather than narrowing a window. Because the flush completes before the rename is issued, and a durable write does not un-happen, the new name can never appear over incomplete bytes. What the missing directory fsync costs is the other direction -- the rename may not survive, so the publish reverts -- and that is the benign one this store already accepts. The module docstring was 76 lines against 49 of everything else, most of it argument rather than documentation: a walk through SQLite's `unixDelete` / `winDelete` sync-flag handling, and a rejected two-slot commit protocol. That is the PR's case for the design, not something to re-read every time someone opens a 20-line module, and the PR body carries it. What a reader here needs is the guarantee, the non-guarantee, and the cost. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL * Say why the swap is a rename, and what the reopened fd cannot see Two things the module was silent on. Why a rename at all. The stronger answer is to put the commit point inside the file, where an fsync reaches it portably -- SQLite never renames, and commits by truncating or zeroing its rollback journal, or in WAL mode by appending frames whose checksums make a torn tail self-identifying. Both need the writer to own the file format. A search engine owns its own and exposes `save(path)`, so above that call a rename is the only atomicity primitive left, and an engine whose format already commits that way needs none of this. Worth saying, because "why not do the better thing" is the first question the module invites. What the reopened descriptor cannot see. Flushing is fine on a fresh fd -- dirty pages belong to the file, not to the descriptor that dirtied them -- but error reporting is not: Linux hands a writeback error to descriptors open when it was recorded, so one recorded between the engine's close and this open is never reported and the save proceeds on bytes already known bad. Same shape as the 2018 PostgreSQL fsync report. It cannot be closed from here: the engine writes through its own descriptor and closes it before returning, and closing the window needs an engine that writes through a handle the caller supplies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL * Fsync the index on a descriptor that predates the write The fsync was on a descriptor opened after the engine had written and closed its own, which flushes correctly -- dirty pages belong to the file, not to the descriptor that dirtied them -- but reports nothing useful. Linux samples the writeback error sequence when a file is opened, so a descriptor opened after an error was recorded never learns of it: the fsync returns success and the save publishes bytes already known bad. Same shape as the 2018 PostgreSQL fsync report. Open the temp before yielding it and hold it across the caller's write, so the descriptor predates the bytes and any error from writing them is reported here, where it fails the save. That assumes the caller writes in place. Both engines do -- verified: the inode is unchanged across `save_index` and `save`, and the held descriptor sees the written size -- but it is their behaviour, not their contract. An engine that built a file of its own and renamed it over the temp would leave this descriptor on an orphaned inode, and the fsync would report on a file nobody is about to publish. So it is checked before the fsync, and a mismatch fails the save rather than passing it quietly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL * State the in-place rule where the caller reads it The descriptor held across the body only flushes what the body wrote if the body writes the yielded path in place, and that requirement was recorded in `_flush_to_disk` -- a private function nobody writing an engine opens. It belongs on `atomic_index_write`, which is the API they use, alongside what happens when it is broken: an `OSError` and no publication, so the mistake surfaces at the first save rather than at a power cut. `_flush_to_disk` keeps the mechanism -- why the descriptor has to predate the write -- and now points at the rule instead of restating it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL * Ask the drive to flush on macOS, where fsync does not `os.fsync` is not the same guarantee on all three platforms this ships to. On Linux it flushes to the device, and on Windows `FlushFileBuffers` does the same. Darwin's `fsync` explicitly does not: it returns once the data reaches the drive, which may hold it in a volatile write cache. So on macOS the ordering this module is built on -- data durable before the rename is issued -- did not hold at the device, which is exactly the case it claims to rule out. `F_FULLFSYNC` asks the drive to flush that cache. Filesystems that cannot refuse it, and there `fsync` is the most that can be asked, so a refusal falls back; any other error is a write failure and propagates, as before. The flush-failure test patched `os.fsync`, which Darwin no longer reaches. It patches the module's own `_fsync` instead, which every platform does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL * Stop guessing which errnos mean "this drive cannot do that" The fallback from `F_FULLFSYNC` to `fsync` was gated on an errno allowlist -- ENOTSUP, EOPNOTSUPP, EINVAL -- so that a genuine write failure would propagate rather than be quietly downgraded. Probing the actual returns on Darwin shows the list is both incomplete and partly invented: ENOTSUP=45 EOPNOTSUPP=102 distinct here, so both are needed /dev/null F_FULLFSYNC -> ENODEV(19), while fsync succeeds pipe/socket F_FULLFSYNC -> EBADF(9) EINVAL never came from F_FULLFSYNC at all; it came from fsync So ENODEV -- a real refusal, on a path anyone can reproduce -- would have raised instead of falling back, and EINVAL was in the list by analogy rather than evidence. What a network mount answers is not knowable from here, which makes the whole list a guess that fails closed on whatever it missed. This codebase does not classify driver errors by guessing, and this was that. Fall through on any failure instead. It is not a suppression: `fsync` runs on the same descriptor and raises in its turn, so a flush that cannot happen still fails the save. What the fallback gives up is the drive-cache flush -- the guarantee this had before `F_FULLFSYNC` was asked for at all. That is also what SQLite does with this same call, for the same reason. The test drives it through `/dev/null`, which refuses with ENODEV and accepts `fsync`; it fails against the allowlist and passes without it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL * Fail the save when the drive cannot be told to flush `F_FULLFSYNC` failing fell through to `fsync`, on the reasoning that a refusal is a statement about the filesystem rather than a write failure. That reasoning does not survive asking what `fsync` alone actually buys on Darwin. Against a process or kernel crash it is enough: the data has left the OS for the drive before the rename is issued, so the rename dies in the page cache and the old index stands. Against power loss it is not. The data sits in the drive's volatile cache, the rename's metadata joins it moments later, and nothing orders them -- and the rename is a few bytes against an index of megabytes, so a drive flushing as it pleases can easily put the new name on media while the bytes behind it are still queued. That is the torn publication this module exists to prevent, in precisely the scenario its docstring is about. So the fallback answered a request for ordering with a flush that does not provide it, and said nothing. A filesystem that cannot order data ahead of a rename is not one to publish an index onto; raise, and let the operator point `index_directory` at storage that can. This is also the simpler code. Refusal and failure now take the same path, so no errno is inspected -- there is no line to draw and no list to get wrong, which is what the previous two revisions kept getting wrong in opposite directions. The `/dev/null` test went with the fallback it pinned. The earlier defence of falling back rested on network and FUSE mounts being a realistic home for an index directory. They are not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NKmF9xNph9QH3ozNw3ZnJL --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 21105d6)
Never reuse a row id in SQLiteVectorStore The records table used a plain INTEGER PRIMARY KEY, which is SQLite's rowid, assigned as max(rowid) + 1: deleting the highest row freed its id for the very next insert. query() scores keys in the search engine and resolves them to rows in a second step, holding nothing in between, so a reused id let a record that was never scored come back wearing the score of the record that was. Nothing about that result looks wrong: the record exists and the score is in range. Declare the table with sqlite_autoincrement=True so ids are never reused. A stale engine key then matches no row and is dropped. Both tests fail without the flag: one pins the id policy directly, the other parks a query between scoring and row lookup, retires the scored record, inserts another, and asserts the query returns nothing. First half of MemMachine#1468. Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…(speedkick) (MemMachine#1612) Own the search engine's concurrency in the store, not in each engine Each engine wrapped its five methods in its own read-write lock and the abstract class promised concurrent use. The store is the only caller, and it already knows which calls must exclude which: a search shares the engine with other searches, and everything else runs alone. Holding that lock in the store puts every exclusion in the one file that reads the engine, and lets a rule the engines could not express hold: a rewrite's remove and add are one step to a reader, where before a search could run between them and see neither version. Engines drop their lock and keep only the index calls; the abstract class now states that the owner serializes. The store keeps one read-write lock per partition life beside its engine, dropped with the engine when the partition is deleted, and takes it at every engine call: searches on the read side; mutations, loads, and the index save on the write side. A rewrite's remove and add sit under one hold. The save's trim runs after the lock is released, so readers wait for the file write and never for SQL. The row-id regression test from MemMachine#1589 parked inside a wrapper engine's search, outside the real engine's lock; under the store's lock that parks the read side, and the writes it then awaits cannot proceed. It now parks where it meant to, between the engine search and the row lookup. One new test pins the one-step rewrite; it fails against the engines' own locks. The turbovec engine in flight carries the same lock and needs the same subtraction. Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…edkick) (MemMachine#1607) Serialize a collection's writes so the engine sees them in order A write commits to SQLite and only then applies to the search engine, so two writers to one uuid could reach the engine in the opposite order to the one they committed in: an upsert overtaking a delete re-adds a vector for a record that is gone, and two upserts inverting leave the engine serving the older vector. Never reusing a row id does not cover this, because an upsert of an existing uuid keeps its row id. A save in that window costs a write outright: it publishes the index and trims every applied log row, and a write that applied after the index was written is then in neither. A per-collection asyncio.Lock now spans a write from SQL commit through engine apply, mark-applied, and any save it triggers; shutdown's save takes it too. The lock belongs to the store, not to a collection handle: a handle is constructed per open_collection call, so several can address one collection, and only a shared lock serializes them. Readers are untouched. Three tests fail without the lock, each interleaving made deterministic by gating the engine: an upsert overtaking a delete of its uuid, a save trimming a write it did not publish, and that overtake across two handles, which a per-handle lock passes. The rest pin behavior the lock must preserve: an upsert surviving a delete of another record, disjoint concurrent upserts and deletes, writes racing a checkpoint, and batches that name one uuid twice. Fixes MemMachine#1468. Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
7c6bc19 to
2a9bfaa
Compare
Port
Copy of #1607, merged into
speedkickas3e816841d, onmain: re-derived onto the partitioned store of #1627. The write lock is keyed by the partition's incarnation, as its engine is, is dropped with the engine when the partition is deleted, and reaches a handle through the store's one handle constructor; the tests build their store through the partition API, and check presence withquery, since #1733 removedget. Nothing else differs.Purpose of the change
A write commits to SQLite and only then applies to the search engine, so two writers to one uuid could reach the engine in the opposite order to the one they committed in: an upsert overtaking a delete re-adds a vector for a record that is gone (it wins result slots and is dropped from them, and the next save publishes it for good), and two upserts inverting leave the engine serving the older vector. Never reusing a row id (#1589) does not cover this: an upsert of an existing uuid keeps its row id.
A save falling in that window costs a write outright. A save publishes the index and then trims every applied log row, and the log is the only other copy of those vectors, so a write that applied after the index was written is then in neither.
A per-partition
asyncio.Lock(one per incarnation,_write_lock_for) now spans a write from SQL commit through engine apply, mark-applied, and any save it triggers; shutdown's save takes it too. The lock belongs to the store, not to a partition handle: a handle is constructed peropen_partitioncall, so several can address one partition, and only a shared lock serializes them. Readers are untouched.Fixes #1468.
Tests
Three fail without the lock, each interleaving made deterministic by gating the engine: an upsert overtaking a delete of the same uuid, a save trimming a write it did not publish, and that overtake across two handles on one partition, which a per-handle lock passes. The interleaving tests poll for what another task has committed; the deadline sits between polls rather than in a timeout around them, because cancelling a query mid-flight leaves its read transaction open on the pooled connection and blocks the next commit.
The rest pin behavior the lock must preserve: an upsert surviving a delete of another record, disjoint concurrent upserts and deletes, writes racing a checkpoint, and batches that name one uuid twice.
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 one commit,
2a9bfaa0e, stacked on #1672. #1674 is stacked on it.Verification
At this PR's head
2a9bfaa0e, on 2026-10-30, rebased after #1628's squash merge (5c9d3ae99), #1831, and #1858. The chain now starts directly onfeat/horizontal-scalingat690116e37instead of on #1844, whose code it does not use. The replay was clean, and each commit's change is the same patch as before. Checks:ruff checkandruff format --checkclean on the changed files at every head;ty checkas CI runs it anduv lock --checkclean at [sqlite store fixes 8] (#1843)'s head, where the server's unit tests pass (2390, 8 skipped). The integration tests did not run locally (Docker was down); CI runs them.Before the rebase onto
690116e37: At this PR's head7c6bc1948, on 2026-10-28, rebased onto [sqlite write transactions 1] (#1844) at4f5d4430c, on [declared schema 1/2] (#1628) atdf716e47b, whose tree differs from the previous base by a docstring and the removedextend_existing=TrueofSQLiteVectorStore._records_table. Each other commit's change is the same patch as before. Checks:ruff checkandruff format --checkclean on the changed files at every head;ty checkas CI runs it anduv lock --checkclean at [sqlite store fixes 8] (#1843)'s head, where the vector store unit tests pass (443, 5 skipped).Before the rebase onto #1844's
4f5d4430c: At this PR's head093d4b709, on 2026-10-27, rebased onto [sqlite write transactions 1] (#1844) at0bcbf8b3a, after [declared schema 1/2] (#1628)'s second round of review changes (274264523), which moveis_valid_identifierand its constants intodata_types.pyand sortvector_store.py's Raises sections. The replay was clean, and each commit's change is the same patch as before. Checks:ruff checkandruff format --checkclean on the changed files at every head;ty checkas CI runs it anduv lock --checkclean at [sqlite store fixes 8] (#1843)'s head, where the vector store unit tests pass (443, 5 skipped).Before the rebase onto #1844's
0bcbf8b3a: At this PR's head8aa774eaa, on 2026-10-27, rebased onto [sqlite write transactions 1] (#1844) at69e292d8a, after [declared schema 1/2] (#1628)'s review changes (d6972a5fa), which dropsupported_filter_nodesand the filter node imports it used, and renamesql_columns.pytosqlite_properties.py. The one conflict was in #1672's commit, at the import lines ofsqlite_vector_store.py; it resolves to #1628'sFilterExprimport plus that commit'sAsyncRWLockimport. Otherwise each commit's change is the same as before, with only context lines moved. Checks:ruff checkandruff format --checkclean on the changed files at every head;ty checkas CI runs it anduv lock --checkclean at [sqlite store fixes 8] (#1843)'s head, where the vector store unit tests pass (443, 5 skipped).Before the rebase onto #1844's
69e292d8a: At this PR's heade5d46594f, on 2026-10-27, rebased when [declared schema 1/2] (#1628) and [sqlite write transactions 1] (#1844) swapped places: #1628 now sits directly onfeat/horizontal-scaling, and #1844 on #1628 at497a89ef1, whose tree is the previous base135184cd6's. This PR's tree is byte-identical to its previous head6a655278f's, so the checks and tests below stand.Before the #1628/#1844 swap: At this PR's head
6a655278f, on 2026-10-27, rebased onto [declared schema 1/2] (#1628) at135184cd6, after #1627 was squash-merged intofeat/horizontal-scalingas805ea2177and [sqlite write transactions 1] (#1844) was rebased onto it as2cb305a8e. Its tree is the previous headd5f351dc3's plus #1628's135184cd6, which gives the Milvus request-size test's store a 120 s request timeout; that test failed on this stack's CI before. Checks at [sqlite store fixes 8] (#1843)'s head:ruff checkandruff format --checkclean;ty checkclean as CI runs it. No test this PR's verification ran changed, so they were not rerun.Before the rebase onto #1628's
135184cd6: At this PR's headd5f351dc3, on 2026-10-26, rebuilt on [declared schema 1/2] (#1628) at4b1210953for #1627's final rebuild, with #1627 at8c2351dbeand [sqlite write transactions 1] (#1844) atc9445910fbelow it. The write lock is keyed by the partition's incarnation and dropped with the engine, and an upsert's stale-handle check stays ahead of its empty-batch return, both inside the lock. The gated-engine helper skips the store's construction-time engine, and the stores it builds get the engine setup the other tests' get. Checks:ruff checkandruff format --checkclean;ty checkclean as CI runs it;uv lock --checkclean. Tests: the SQLite store's and the search engines' tests, 210 tests. The integration tests did not run locally (Docker was down); CI runs them.Before the rebuild on #1628's
4b1210953: At this PR's head324235939, on 2026-10-20, its tree is the previous headc5fce820d'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. No integration tests ran this time, since only comments, docstrings, descriptions, and documentation changed.Before #1627's accuracy fixes: At this PR's head
c5fce820d, on 2026-10-19, its tree is the previous head73f0f4249'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.Before #1627's second review's fixes: At this PR's head
73f0f4249, on 2026-10-19, its tree is the previous head39f480aff'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.Before #1627's review fixes: At this PR's head
39f480aff, on 2026-10-19, moved onto [declared schema 1/2] (#1628): its tree is the previous headbd05ccc46's without [search results] (#1663) beneath it. Its tests say "score threshold" where they said "minimum cosine similarity". 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, 376 tests (5 skipped).Before the move off [search results] (#1663): At this PR's head
bd05ccc46, on 2026-10-19, its tree is the previous head78996b730'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
78996b730, on 2026-10-19, its tree is the previous head0bf8ffb86'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.Before the rebase onto #1824's merge: At this PR's head
0bf8ffb86, on 2026-10-16, its tree is the previous head1997f88c4'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.Before the retention check's rename: At this PR's head
1997f88c4, on 2026-10-16, its tree is the previous headc6ed3a405'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.Before the raising checks took the
validate_prefix: At this PR's headc6ed3a405, on 2026-10-16, its tree is the previous head8f978a5bf'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.Before the store's properties left its interface: At this PR's head
8f978a5bf, on 2026-10-15: its tree is the previous headdee3bc33d'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.Before the per-partition schema record went: At this PR's head
dee3bc33d, on 2026-10-15: its tree is the previous head84274b98b'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
84274b98b, on 2026-10-15: its tree is the previous head8482414eb'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 head8482414eb, on 2026-10-15, restacked on #1627's rewritten history: its tree is the previous headd7fc3c0f4'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
d7fc3c0f4, 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.Before the rebase onto
cf7ae1579: At this PR's head47e1c8b21, 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
c394d566c, 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.Before the rebuild on #1702's filter routing: At this PR's head
83262eeb3, 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.Before the rebase onto #1813's merge: At this PR's head
54bee6f87, 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; the SQLite stores' and search engines' tests pass, 245 tests. The full suites last passed at11b014a14, on 2026-10-08, before #1628 moved beneath this PR: the server suite without integration tests, 2120 tests, and the integration tests of the vector stores, the resource manager, episodic memory, and semantic storage, 668 tests, against PostgreSQL 16, Neo4j, Qdrant 1.19.1, and Milvus 2.6.24 in containers.🤖 Generated with Claude Code
https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn