fix: vec index usable across threads — phone-capture approve crashed - #42
Conversation
…41) The GUI creates the repository (and the sqlite-vec connection, #35) on the main thread while the capture worker performs every assess/commit; sqlite3 refuses cross-thread connection use by default, so the first indexed write raised sqlite3.ProgrammingError and left every later capture in the session failing. Open the connection with check_same_thread=False and serialize all DB access — including the _dim/_degraded state — under one re-entrant lock. Regression test drives add_note/most_similar from a thread the index was not created on.
|
Automated review (Claude code-review sub-agent) Verdict: request changes (one HIGH scope gap; the core cross-thread fix itself is sound) Ran [HIGH] The fix only covers the vec-index's own sqlite state — the wrapped inner [LOW] Other checks — clean: (a) every |
|
Maintainer response to the automated review above The HIGH finding is real but pre-existing, not introduced here: without the optional Scoped disposition:
Both LOW notes from the first review are also folded into #44. |
Fixes #41.
What broke
grandplan gui --servewith phone captures: the first approve crashed withsqlite3.ProgrammingError: SQLite objects created in a thread can only be used in that same thread,and every subsequent capture in the session then failed during analysis. The capture pipeline was
effectively down (notes were never lost — the JSONL event log is the store; the vec db is a
rebuildable index that resyncs on reopen).
Root cause
gui.pybuilds the repository — andVecIndexedRepository's sqlite connection (#35) — on themain thread; the coordinator's
grandplan-captureworker thread performs everyassess/commit. Python's sqlite3 refuses cross-thread connection use by default. It only surfaced at
approve because a fresh index has no recorded dim, so
most_similarsilently fell back to bruteforce until the first real DB write.
Fix
sqlite3.connect(..., check_same_thread=False)+ onethreading.RLockserializing every DBaccess and the
_dim/_degradedstate it guards (re-entrant:_sync → _indexnests).Test plan
test_usable_from_threads_other_than_the_creating_one— creates theindex on one thread, drives
add_note/most_similarfrom another; failed with the exactproduction error before the fix (RED), passes after (GREEN).
concurrent-race test;
_syncholds the lock across inner-repo calls — safe today, both fine asfuture work).