Repository navigation
Conversation
edwinyyyu
force-pushed
the
fix/sqlite-vector-store-row-id-speedkick
branch
5 times, most recently
from
September 10, 2026 21:15
7b170bc to
32f875f
Compare
This was referenced Sep 10, 2026
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. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
edwinyyyu
force-pushed
the
fix/sqlite-vector-store-row-id-speedkick
branch
from
September 10, 2026 21:42
32f875f to
cda8a80
Compare
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 10, 2026
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, per collection, the engine together with its lock, created and dropped with the engine, and every engine call sits under it: searches on the read side, mutations, loads, and the index save on the write side. 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. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 10, 2026
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 wraps each collection's engine in a _SynchronizedEngine, created and dropped with the engine, whose methods apply the rule: search on the read side; replace, remove, save, and load on the write side. A rewrite is one replace. 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. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 11, 2026
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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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. Co-Authored-By: Claude Fable 5.1 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
edwinyyyu
added a commit
that referenced
this pull request
Sep 11, 2026
…(speedkick) (#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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 #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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 16, 2026
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 <[email protected]>
This was referenced Sep 16, 2026
Merged
Closed
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
This was referenced Sep 17, 2026
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 17, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 18, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 18, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 18, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 18, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 21, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 21, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 21, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 21, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 25, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Sep 25, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Oct 10, 2026
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 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Oct 10, 2026
…(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 collection beside the engine, kept for the store's lifetime like the engine's other per-collection state, 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 <[email protected]>
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
SQLiteVectorStoreCollectionkeeps records in SQLite and vectors in a search engine keyed by the record's row id. The records table used a plainINTEGER PRIMARY KEY, which is SQLite's rowid, assigned asmax(rowid) + 1: deleting the highest row freed its id for the very next insert, so a new record inherited the id an old one was being served under.queryscores keys in the search engine, then resolves them to rows in a second step, holding nothing in between, because readers are not made to wait on writers. A reused id lets a record that was never scored come back wearing the score of the record that was. Nothing about that result looks wrong from outside: the record exists, the score is in range, and no row or vector is left dangling to find the mix-up by.sqlite_autoincrement=True, so ids are never reused. A stale engine key then matches no row and is dropped.First half of #1468. The other half, concurrent writers reaching the engine out of commit order, is #1607, stacked on this.
Tests
Both fail without the flag.
test_row_ids_are_never_reusedpins the id policy directly.test_a_query_cannot_return_a_record_it_never_scoredgates the engine so a query parks between scoring and row lookup, deletes the scored record and inserts another in that window, and asserts the query returns nothing.Verification
test_sqlite_vector_store.py: 72 passed. The 2 new tests fail againstspeedkick's store.ruff check/ruff format --check: clean.Bottom of a four-PR stack (this → #1607 → #1608 → #1609), split from the earlier single PR for readability.
🤖 Generated with Claude Code
https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn