fix(state): probe FTS5 read path in _db_opens_cleanly so partial index corruption is detected (#66724)

`hermes sessions repair --check-only` opens cleanly on state.db files
with partial FTS5 index corruption — base tables read fine, the rolled-back
write probe from #50502 succeeds, and `PRAGMA integrity_check` returns
"ok". But every session_search / /resume title resolution / feature
backed by MATCH / snippet / rank queries errors out with
`database disk image is malformed` because internal shadow-table segments
are bad. The official repair tool then gives false confidence.

Add a representative FTS5 read probe against both `messages_fts` and
`messages_fts_trigram` (the latter backs title resolution). Empty MATCH
strings are accepted by every FTS5 index without requiring populated
content, so the probe is safe on a freshly-init'd DB; missing-table /
missing-column errors fall through to the existing "not yet a populated
DB" branch, matching the write-probe's behaviour. Any other OperationalError
is surfaced as the check reason, which sends `hermes sessions repair` to
its existing FTS 'rebuild' path (repair_state_db_schema, line 616).

Single-file change in hermes_state.py::_db_opens_cleanly. No public API
change. No new imports. Fixes #66724.
This commit is contained in:
Enough1122 2026-07-18 20:10:03 +08:00 committed by Teknium
parent 5e4529c02f
commit 11e76f4e01

View file

@ -579,6 +579,36 @@ def _db_opens_cleanly(db_path: Path) -> Optional[str]:
return "; ".join(problems[:3])
conn.execute("SELECT COUNT(*) FROM sessions").fetchone()
# FTS5 read probe: run a representative MATCH query against the
# messages_fts* virtual tables. The FTS *write* probe above catches
# the corruption class where base tables read fine but writes fail
# through the triggers (#50502). It does NOT catch partial FTS5
# index corruption — bad shadow-table segments where reads still
# parse but MATCH / snippet / rank queries error out with
# "database disk image is malformed". session_search, /resume title
# resolution, and any feature relying on FTS5 discovery then break
# silently because the official repair tool's check-only path
# reports the DB as healthy. #66724.
try:
for fts_table in ("messages_fts", "messages_fts_trigram"):
# No-op queries against the actual FTS5 APIs the search
# tools use. The trigram table is included because it backs
# the title-resolution path; either corruption mode would
# break session recall without this probe. Empty MATCH
# string is the safest probe — every FTS5 index accepts it
# without requiring populated content.
conn.execute(
f"SELECT 1 FROM {fts_table} WHERE {fts_table} MATCH '' LIMIT 1"
).fetchone()
except sqlite3.OperationalError as exc:
msg = str(exc).lower()
if "no such table" in msg or "no such column" in msg:
# FTS5 not built yet (brand new file mid-init) — not the
# corruption class we probe.
pass
else:
return f"fts5 read probe failed on {fts_table}: {exc}"
# FTS write probe: drive a row through the messages_fts* triggers in a
# transaction that is always rolled back, so a corrupt FTS index that
# rejects writes is caught even though reads look healthy. The probe is