From 505fb587512301346c7408ba840c3d269bb94519 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Mon, 13 Jul 2026 04:38:39 +0800 Subject: [PATCH] fix(state): add REINDEX strategy to repair stale B-tree indexes (#63386) When PRAGMA integrity_check reports 'wrong # of entries in index' for B-tree indexes (e.g. idx_sessions_handoff_state), the existing repair strategies (FTS rebuild, sqlite_master dedup, drop-FTS+VACUUM) don't address the mismatch. Add Strategy 0.5: run REINDEX to rewrite the index b-tree from canonical table rows before escalating to more destructive strategies. --- hermes_state.py | 23 ++++++++++++++++++++ tests/test_state_db_malformed_repair.py | 29 +++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/hermes_state.py b/hermes_state.py index 97fbb40439..ef84435ea0 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -748,6 +748,29 @@ def repair_state_db_schema(db_path: Path, *, backup: bool = True) -> Dict[str, A except sqlite3.DatabaseError as exc: logger.warning("state.db FTS in-place rebuild pass failed: %s", exc) + # ── Strategy 0.5: rebuild stale B-tree indexes (#63386) ── + # PRAGMA integrity_check can report "wrong # of entries in index" when a + # B-tree index (e.g. idx_sessions_handoff_state) falls out of sync with its + # base table. REINDEX rewrites the index b-tree from the canonical table + # rows using the existing index definition, fixing the mismatch without + # touching data or FTS schema. + try: + conn = sqlite3.connect(str(db_path), isolation_level=None) + try: + conn.execute("REINDEX") + conn.commit() + finally: + conn.close() + if _db_opens_cleanly(db_path) is None: + report["repaired"] = True + report["strategy"] = "reindex_btree" + logger.warning( + "state.db B-tree indexes rebuilt via REINDEX: %s", db_path + ) + return report + except sqlite3.DatabaseError as exc: + logger.warning("state.db REINDEX pass failed: %s", exc) + # ── Strategy 1: de-duplicate sqlite_master (keeps FTS index) ── try: conn = sqlite3.connect(str(db_path), isolation_level=None) diff --git a/tests/test_state_db_malformed_repair.py b/tests/test_state_db_malformed_repair.py index 427496d203..d46a6419d8 100644 --- a/tests/test_state_db_malformed_repair.py +++ b/tests/test_state_db_malformed_repair.py @@ -487,6 +487,35 @@ def test_repair_noop_db_uses_already_healthy_shortcut(tmp_path): assert report["strategy"] == "already_healthy" +def test_repair_rebuilds_stale_btree_indexes(tmp_path, monkeypatch): + """repair_state_db_schema uses REINDEX for 'wrong # of entries in index'. + + When PRAGMA integrity_check reports a stale B-tree index (e.g. + idx_sessions_handoff_state), the FTS-rebuild and dedup strategies don't + help — REINDEX rewrites the index b-tree from the canonical table rows. + """ + db_path = tmp_path / "state.db" + _build_healthy_db(db_path) + + _reason = "wrong # of entries in index idx_sessions_handoff_state" + _call_count = {"n": 0} + _real_check = hermes_state._db_opens_cleanly + + def _simulated_check(path): + _call_count["n"] += 1 + # initial health check + post-Strategy-0 check report corruption; + # after REINDEX (Strategy 0.5) the DB is healthy. + if _call_count["n"] <= 2: + return _reason + return _real_check(path) + + monkeypatch.setattr(hermes_state, "_db_opens_cleanly", _simulated_check) + + report = repair_state_db_schema(db_path) + assert report["repaired"] is True + assert report["strategy"] == "reindex_btree" + + def test_select_cached_agent_history_prefers_longer_live_transcript(): """Gateway guard keeps the live transcript when persisted history lags.""" from gateway.run import _select_cached_agent_history