fix(state): quarantine SessionDB handle after structural corruption

A bare SQLITE_CORRUPT/NOTADB on a live write (not FTS-scoped, not a
replaced file) now sets a sticky per-instance flag: later writes fail
fast with StateDbCorruptError, the handle never reopens after close(),
and close() skips its explicit PASSIVE WAL checkpoint. Gateway and agent
flush paths divert pending transcripts to JSONL/spool like the replaced
case instead of retrying forever.

Field evidence: a handle that kept writing for ~50 minutes after the
first structural error checkpointed 15 pages under the wrong page
numbers on shutdown (page 1 <- messages_fts_trigram_data leaf), turning
"malformed" into "file is not a database".

Refs #90837, #90950, #97940, #89332, #45383

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CNX8rNYHqA5pT4tAGSzXtb
This commit is contained in:
leomcamilo
2026-09-02 05:22:41 -03:00
committed by kshitij
parent d8616f1c88
commit bcc2e65818
10 changed files with 565 additions and 11 deletions
+5 -2
View File
@@ -14,7 +14,7 @@ from unittest.mock import MagicMock
import pytest
from hermes_state import SessionDB, _on_disk_journal_mode
from hermes_state import SessionDB, StateDbCorruptError, _on_disk_journal_mode
class _NotADbOnce:
@@ -42,9 +42,12 @@ class TestFailClosedAfterNotADb:
reopen = MagicMock()
monkeypatch.setattr("hermes_state._connect_tracked_db", reopen)
db._conn = _NotADbOnce(real_conn)
with pytest.raises(sqlite3.DatabaseError, match="not a database"):
with pytest.raises(sqlite3.DatabaseError, match="not a database") as excinfo:
db.create_session(session_id="s2", source="cli", model="test")
reopen.assert_not_called()
# NOTADB on a live write is structural: the handle is quarantined.
assert isinstance(excinfo.value, StateDbCorruptError)
assert db._db_corrupt is True
finally:
db._conn = real_conn
db.close()