Severity: high
Location: better_memory/services/semantic.py:101 (+ reflection.py:1235, :1729)
Merged with the synthesize_next_apply finding — same defect, five call sites.
What breaks. Python's sqlite3 in default isolation mode issues an implicit BEGIN before the first DML (connection.py:29-31); in WAL mode that takes the single writer lock until commit(). These paths open the transaction, then call the blocking SyncEmbedder, then commit:
| Path |
DML |
Embed |
Commit |
SemanticMemoryService.create |
semantic.py:91 |
:101 |
:102 |
SemanticMemoryService.update_text |
:109 |
:123 |
:124 |
create_from_observation |
SAVEPOINT :185 |
:203 |
:210 |
ReflectionSynthesisService.apply_decision |
SAVEPOINT reflection.py:1235 |
:835/:991 (N embeds) |
:1256 |
ReflectionService.edit_text (UI) |
reflection.py:1729 |
:1738 |
:1742 |
Each embed can hold for up to 15 s (sync_embed.py:37, async_bridge.py:59); apply_decision does one per reflection. Every other connection gets PRAGMA busy_timeout=5000 (connection.py:54) — 5 s of patience. This is genuinely cross-process: the MCP server owns one connection (server.py:165), the Flask UI opens its own (ui/app.py:102), both wired to SyncEmbedder(lambda: OllamaEmbedder(timeout=5.0, max_retries=1)) (server.py:188-190, ui/app.py:49). A refuter noted the INSERT at reflection.py:802 precedes the embed at :835, so the lock is provably held across the network call even for N=1. A refuter reproduced the mechanism on the project interpreter (Python 3.12.10 / SQLite 3.49.1) with the project's own PRAGMAs: connection B died with sqlite3.OperationalError: database is locked after 5.5 s.
Failure scenario. synthesize_next_apply with 4-5 new reflections while Ollama loads a model (~4-10 s per embed). The savepoint holds the writer lock for 16-50 s. The management UI's "confirm reflection" POST exceeds its 5000 ms busy_timeout and returns HTTP 500. Same in reverse when the UI's edit_text holds the lock. If a worker exceeds 15 s, run_async_in_worker abandons the daemon thread (async_bridge.py:18-24, 60-63) with the transaction still open.
Fix. Compute the embedding before opening the transaction (or after committing, then write the vector in a second short transaction) so no network call happens inside a write lock.
Filed from an automated adversarial bug hunt (bughunt workflow, run
wf_43511264-124, 87 agents). Each finding was attacked by three independent
refuters from different angles — reproduce, reachability, and library/language
semantics — and survived a majority. Where a refuter corrected or narrowed the
claim, that correction is preserved inline above rather than dropped. Line
numbers reflect the tree at the time of the run; verify before acting.
Severity: high
Location:
better_memory/services/semantic.py:101 (+ reflection.py:1235, :1729)Merged with the
synthesize_next_applyfinding — same defect, five call sites.What breaks. Python's sqlite3 in default isolation mode issues an implicit BEGIN before the first DML (connection.py:29-31); in WAL mode that takes the single writer lock until
commit(). These paths open the transaction, then call the blockingSyncEmbedder, then commit:SemanticMemoryService.createSemanticMemoryService.update_textcreate_from_observationReflectionSynthesisService.apply_decisionReflectionService.edit_text(UI)Each embed can hold for up to 15 s (sync_embed.py:37, async_bridge.py:59);
apply_decisiondoes one per reflection. Every other connection getsPRAGMA busy_timeout=5000(connection.py:54) — 5 s of patience. This is genuinely cross-process: the MCP server owns one connection (server.py:165), the Flask UI opens its own (ui/app.py:102), both wired toSyncEmbedder(lambda: OllamaEmbedder(timeout=5.0, max_retries=1))(server.py:188-190, ui/app.py:49). A refuter noted theINSERTat reflection.py:802 precedes the embed at :835, so the lock is provably held across the network call even for N=1. A refuter reproduced the mechanism on the project interpreter (Python 3.12.10 / SQLite 3.49.1) with the project's own PRAGMAs: connection B died withsqlite3.OperationalError: database is lockedafter 5.5 s.Failure scenario.
synthesize_next_applywith 4-5 new reflections while Ollama loads a model (~4-10 s per embed). The savepoint holds the writer lock for 16-50 s. The management UI's "confirm reflection" POST exceeds its 5000 ms busy_timeout and returns HTTP 500. Same in reverse when the UI'sedit_textholds the lock. If a worker exceeds 15 s,run_async_in_workerabandons the daemon thread (async_bridge.py:18-24, 60-63) with the transaction still open.Fix. Compute the embedding before opening the transaction (or after committing, then write the vector in a second short transaction) so no network call happens inside a write lock.
Filed from an automated adversarial bug hunt (
bughuntworkflow, runwf_43511264-124, 87 agents). Each finding was attacked by three independentrefuters from different angles — reproduce, reachability, and library/language
semantics — and survived a majority. Where a refuter corrected or narrowed the
claim, that correction is preserved inline above rather than dropped. Line
numbers reflect the tree at the time of the run; verify before acting.