From 9ae98e07a7ee7929f8ec3902c545c42d66f10268 Mon Sep 17 00:00:00 2001 From: channkim Date: Tue, 16 Jun 2026 14:06:26 +0900 Subject: [PATCH] fix(agent): rebuild base fts without trigram --- hermes_state.py | 19 ++++++++++++++----- tests/test_hermes_state.py | 39 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index 99cb24748e6..36e5c91fe8a 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -845,9 +845,12 @@ class SessionDB: return int(row[0] if not isinstance(row, sqlite3.Row) else row[0]) @staticmethod - def _rebuild_fts_indexes(cursor: sqlite3.Cursor) -> None: - for table_name in ("messages_fts", "messages_fts_trigram"): - cursor.execute(f"DELETE FROM {table_name}") + def _rebuild_fts_indexes( + cursor: sqlite3.Cursor, + *, + include_trigram: bool = True, + ) -> None: + cursor.execute("DELETE FROM messages_fts") cursor.execute( "INSERT INTO messages_fts(rowid, content) " "SELECT id, " @@ -856,6 +859,9 @@ class SessionDB: "COALESCE(tool_calls, '') " "FROM messages" ) + if not include_trigram: + return + cursor.execute("DELETE FROM messages_fts_trigram") cursor.execute( "INSERT INTO messages_fts_trigram(rowid, content) " "SELECT id, " @@ -1317,8 +1323,11 @@ class SessionDB: cursor, "messages_fts_trigram", FTS_TRIGRAM_SQL ) self._trigram_available = trigram_enabled - if trigram_enabled and triggers_need_repair: - self._rebuild_fts_indexes(cursor) + if triggers_need_repair: + self._rebuild_fts_indexes( + cursor, + include_trigram=trigram_enabled, + ) self._conn.commit() diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index 0baf3226401..e4650ed5dc7 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -344,6 +344,45 @@ class TestSessionLifecycle: finally: restored.close() + def test_base_fts_rebuilds_after_trigger_repair_without_trigram( + self, tmp_path, monkeypatch + ): + """Trigger repair must rebuild base FTS even when trigram is unavailable.""" + db_path = tmp_path / "state.db" + seeded = SessionDB(db_path=db_path) + try: + seeded.create_session(session_id="s1", source="cli") + seeded.append_message("s1", role="user", content="already indexed") + for trigger in ( + "messages_fts_insert", + "messages_fts_delete", + "messages_fts_update", + "messages_fts_trigram_insert", + "messages_fts_trigram_delete", + "messages_fts_trigram_update", + ): + seeded._conn.execute(f"DROP TRIGGER IF EXISTS {trigger}") + seeded._conn.commit() + seeded.append_message("s1", role="assistant", content="repair only base needle") + finally: + seeded.close() + + real_connect = sqlite3.connect + + def connect_without_trigram(*args, **kwargs): + kwargs["factory"] = _NoTrigramConnection + return real_connect(*args, **kwargs) + + monkeypatch.setattr("hermes_state.sqlite3.connect", connect_without_trigram) + restored = SessionDB(db_path=db_path) + try: + assert restored._fts_enabled is True + assert restored._trigram_available is False + assert restored._fts_table_exists("messages_fts") is True + assert len(restored.search_messages("needle")) == 1 + finally: + restored.close() + def test_is_fts5_unavailable_error_catches_trigram_tokenizer(self): """Unit test: _is_fts5_unavailable_error matches 'no such tokenizer: trigram'.""" fts5_err = sqlite3.OperationalError("no such module: fts5")