From b6d2a2ef177bc7a779edb541a6f0a4e9e5a6f2ee Mon Sep 17 00:00:00 2001 From: nesquena-hermes Date: Tue, 22 Sep 2026 22:55:41 +0000 Subject: [PATCH] fix(state-db): skip read indexes an older schema can't hold instead of rolling back the rest On a pre-2026-07-28 Agent schema, sessions has no last_activity_at, so the fingerprint index's CREATE failed inside the single BEGIN IMMEDIATE and rolled back idx_messages_session too, leaving messages unindexed: the exact older-agent case the troubleshooting doc says the tool fixes. Gate each index on its columns being present and report 'skipped'. (Opus advisor SHOULD-FIX on #7445.) --- scripts/ensure_state_db_read_indexes.py | 8 +++++++ .../test_pr7445_readonly_review_followups.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/scripts/ensure_state_db_read_indexes.py b/scripts/ensure_state_db_read_indexes.py index 31dce342..8d38b8a7 100644 --- a/scripts/ensure_state_db_read_indexes.py +++ b/scripts/ensure_state_db_read_indexes.py @@ -92,6 +92,14 @@ def ensure_read_indexes(db_path, *, confirmed_drained=False, lock_file=None): db.execute("BEGIN IMMEDIATE") try: for name, (table, keys) in INDEXES.items(): + # An older Agent schema can lack a column one index keys on + # (sessions.last_activity_at is newer than + # messages.timestamp). Skip that index instead of letting its + # CREATE fail and roll back the ones this schema supports. + present = {r[1] for r in db.execute(f"PRAGMA table_info({table})")} + if any(col not in present for col, _ in keys): + statuses[name] = "skipped" + continue existing = db.execute("SELECT tbl_name FROM sqlite_master WHERE name=?", (name,)).fetchone() if existing: actual = tuple((r[2], r[4]) for r in db.execute(f"PRAGMA index_xinfo({name})") if r[5]) diff --git a/tests/test_pr7445_readonly_review_followups.py b/tests/test_pr7445_readonly_review_followups.py index 683cc072..665f0eae 100644 --- a/tests/test_pr7445_readonly_review_followups.py +++ b/tests/test_pr7445_readonly_review_followups.py @@ -248,6 +248,30 @@ def _maintenance_schema(path): ) +def test_maintenance_skips_indexes_an_older_schema_cannot_hold(tmp_path, maintenance_without_fcntl): + """A legacy ``sessions`` table without ``last_activity_at`` must not roll back + the message indexes the schema does support.""" + module = maintenance_without_fcntl + path = tmp_path / "state.db" + with closing(sqlite3.connect(str(path))) as conn: + conn.executescript( + """ + CREATE TABLE sessions(source TEXT, id TEXT, message_count INTEGER); + CREATE TABLE messages(session_id TEXT, timestamp REAL, role TEXT); + """ + ) + result = module.ensure_read_indexes(path, confirmed_drained=True) + assert result == { + "idx_messages_session": "created", + "idx_messages_session_role": "created", + "idx_sessions_webui_fingerprint": "skipped", + } + with closing(sqlite3.connect(str(path))) as conn: + names = {r[0] for r in conn.execute("SELECT name FROM sqlite_master WHERE type='index'")} + assert {"idx_messages_session", "idx_messages_session_role"} <= names + assert "idx_sessions_webui_fingerprint" not in names + + @pytest.fixture def maintenance_without_fcntl(monkeypatch): """Import the maintenance module as a platform without ``fcntl`` sees it."""