Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion hermes_state_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,8 @@ def rebuild_fts(self) -> int:

Uses the FTS5 ``'rebuild'`` command, which rewrites the internal b-tree segments from the content
rows. Unlike ``optimize_fts`` (which merges existing segments), ``rebuild`` discards and recreates
the index data entirely. See #50502.
the index data entirely — the more destructive of the two, so it is quarantined the same way. See
#50502.
A full structural rebuild must never run concurrently in two processes sharing one state.db — that
interleaving has structurally corrupted the database in production (PR #93200) — so this admits
through the cross-process ``fts_rebuild_admission`` authority and FAILS CLOSED: if another process
Expand All @@ -1217,6 +1218,8 @@ def rebuild_fts(self) -> int:
path, which retries in-process from the gateway housekeeping tick (``retry_deferred_fts_recovery``)
and at next startup.
"""
self._raise_if_db_corrupt()
self._raise_if_db_replaced()
rebuilt = 0
with fts_rebuild_admission(self.db_path) as admitted:
if not admitted:
Expand Down
23 changes: 23 additions & 0 deletions tests/hermes_state/test_state_db_corrupt_quarantine.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,26 @@ def test_optimize_fts_refuses_when_quarantined(self, tmp_path, flag_name, expect
_clear_flag(db, flag_name)
db._conn = real_conn
db.close()

@pytest.mark.parametrize("flag_name,expected_exc", _QUARANTINE_FLAGS)
def test_rebuild_fts_refuses_when_quarantined(self, tmp_path, flag_name, expected_exc):
"""rebuild_fts() is reachable outside _execute_write's own quarantine check — the gateway's
FTS-corruption transcript-retry path (gateway/session_transcript.py::_rebuild_fts_once)
calls it directly. Unlike optimize_fts ("merges existing segments"), rebuild_fts "discards
and recreates the index data entirely" — strictly more destructive — so it must refuse at
least as eagerly."""
db = SessionDB(db_path=tmp_path / "state.db")
real_conn = db._conn
try:
db.create_session(session_id="s1", source="cli", model="test")
db.append_message("s1", role="user", content="hello world")
recorder = _RecordingConn(real_conn)
db._conn = recorder
_force_flag(db, flag_name)
with pytest.raises(expected_exc):
db.rebuild_fts()
assert recorder.recorded == []
finally:
_clear_flag(db, flag_name)
db._conn = real_conn
db.close()