{{ message }}
Repository navigation
fix(core): replace an entity's search rows in one transaction - #1623
Merged
Merged
Conversation
SearchService.index_entity_data deleted the entity's search rows with a committed delete, then wrote the replacement rows (and on Postgres their FTS chunks) in a later transaction. A timeout while writing the replacement left an existing note with no search rows. Nothing rebuilds a missing projection: the orphan sweep only removes, and readiness counts files by checksum, so the note stayed unsearchable until edited or reindexed. delete_by_entity_id, index_item and bulk_index_items now accept an optional caller-owned session and leave the commit to scoped_session, and index_entity_data runs the delete and the replacement in one session. A failure rolls back to the previous projection, or to none on a first index. Fixes #1621 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BF4TkgAJpVrgwzrhemkdig Signed-off-by: phernandez <paul@basicmachines.co>
saintar1997
pushed a commit
to saintar1997/basic-memory
that referenced
this pull request
Oct 6, 2026
Tests for behaviors the downstream repair asked about that basicmachines-co#1623 already provides, so they stay provided: - a vector sync that runs while another writer's refresh is open keeps the note's vectors, and full-text search still finds it (SQLite and PostgreSQL, Markdown and file notes); - refreshes queued behind an open refresh leave one projection; - a file without a permalink keeps one row across refreshes, a renamed permalink leaves no old rows (or PostgreSQL full-text chunks), and a Markdown note re-indexed as a file keeps only its entity row. Also drops the WIP evidence directory; it stays in this branch's history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TmJtSr6Mu7ZgWWpZr2S37B
saintar1997
pushed a commit
to saintar1997/basic-memory
that referenced
this pull request
Oct 6, 2026
Tests for behaviors the downstream repair asked about that basicmachines-co#1623 already provides, so they stay provided: - a vector sync that runs while another writer's refresh is open keeps the note's vectors, and full-text search still finds it (SQLite and PostgreSQL, Markdown and file notes); - refreshes queued behind an open refresh leave one projection; - a file without a permalink keeps one row across refreshes, a renamed permalink leaves no old rows (or PostgreSQL full-text chunks), and a Markdown note re-indexed as a file keeps only its entity row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TmJtSr6Mu7ZgWWpZr2S37B
This was referenced Oct 8, 2026
Closed
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.

Summary
A failed search refresh could remove a note from search. Fixes #1621.
Cause
SearchService.index_entity_dataran in two transactions:repository.delete_by_entity_id, which committed on its own. On Postgres,search_index_fts_chunksrows go with it through the cascade.index_entity_markdownorindex_entity_file, which wrote the replacement rows (and on Postgres_replace_fts_chunks) in a later transaction.If step 2 fails, as in the production
_replace_fts_chunkstimeouts, the note is left with no search rows at all.Under the AGENTS.md consistency model this is a real finding, not temporary lag. The state doesn't converge by itself:
The note stays out of search until someone edits it or re-indexes the project.
Fix
delete_by_entity_id,index_itemandbulk_index_itemstake an optional caller-ownedsession. This applies to the Protocol, the base class, and the SQLite and Postgres implementations.session.commit()calls are gone. When the method opens its own session,db.scoped_sessionalready commits on success and rolls back on error.index_entity_dataruns the delete and the replacement in onescoped_session.Result:
This adds no
FOR UPDATEand no cross-entity transaction. The only new lock is the delete's lock on this entity's own rows, held until the replacement commits. Postgres already ran the upsert and chunk replacement in a single transaction.Not in this PR
search_index_fts_chunksbloat and autovacuum pressure.Tests
New file
tests/services/test_search_refresh_atomicity.py:_replace_fts_chunkstimes out. The previous rows and chunk count are unchanged, and a retry keeps the chunk count stable.The first two tests fail on main.
testspasses on SQLite (7920 passed).tests/repository,tests/indexingandtests/servicespass on Postgres (2389 passed).just fast-checkis clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01BF4TkgAJpVrgwzrhemkdig