mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(state): heal NULL active rows on every startup, not just pre-v12 DBs
The repair UPDATE ('SET active = 1 WHERE active IS NULL') was gated at
schema_version < 12, so already-v12+ databases — the exact population hit
by #51646, where the reconciler-added active column lacks its NOT NULL
DEFAULT 1 — never healed rows written as NULL by the pre-fix INSERTs.
Move the idempotent repair into unconditional startup so historical
gateway transcripts become visible again after upgrading.
Follow-up to the salvage of #59832 by @HexLab98.
This commit is contained in:
parent
7445df1505
commit
b75783e6dc
2 changed files with 71 additions and 12 deletions
|
|
@ -1381,6 +1381,23 @@ class SessionDB:
|
|||
# column (idx_messages_session_active) — same ordering constraint.
|
||||
cursor.executescript(DEFERRED_INDEX_SQL)
|
||||
|
||||
# Heal NULL ``active`` rows unconditionally on every startup.
|
||||
# On real-world DBs the reconciler-added ``active`` column can lack
|
||||
# its NOT NULL DEFAULT 1 (older reconciler builds reconstructed the
|
||||
# type without the default — see #51646: PRAGMA shows
|
||||
# (17,'active','INTEGER',0,None,0) in the wild), so INSERTs that
|
||||
# omitted the column wrote NULL and the ``WHERE active = 1``
|
||||
# transcript loaders hid the whole history. The INSERTs now set
|
||||
# active=1 explicitly; this idempotent repair un-hides rows written
|
||||
# before the fix. It was previously gated at ``current_version <
|
||||
# 12`` which never re-ran for already-v12+ databases.
|
||||
try:
|
||||
cursor.execute(
|
||||
"UPDATE messages SET active = 1 WHERE active IS NULL"
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
|
||||
fts5_available = self._sqlite_supports_fts5(cursor)
|
||||
fts_migrations_complete = True
|
||||
if not fts5_available:
|
||||
|
|
@ -1495,18 +1512,6 @@ class SessionDB:
|
|||
fts_migrations_complete = False
|
||||
else:
|
||||
fts_migrations_complete = False
|
||||
if current_version < 12:
|
||||
# v12: messages.active flag for rewind/undo soft-deletion.
|
||||
# The declarative reconcile_columns() above adds the
|
||||
# column itself; this UPDATE is belt-and-suspenders to
|
||||
# ensure any rows that pre-existed the ADD COLUMN have
|
||||
# active=1 rather than NULL.
|
||||
try:
|
||||
cursor.execute(
|
||||
"UPDATE messages SET active = 1 WHERE active IS NULL"
|
||||
)
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
if current_version < 16:
|
||||
# v16: tag delegate subagent rows so pickers stay clean after
|
||||
# parent deletes that used to orphan them (parent_session_id → NULL).
|
||||
|
|
|
|||
|
|
@ -717,6 +717,60 @@ class TestMessageStorage:
|
|||
finally:
|
||||
session_db.close()
|
||||
|
||||
def test_startup_heals_null_active_rows(self, tmp_path):
|
||||
"""Rows written as active=NULL before the fix are un-hidden on startup.
|
||||
|
||||
The repair UPDATE used to be gated at schema_version < 12, so
|
||||
already-v12+ databases (the exact population hit by #51646) never
|
||||
healed their historical NULL rows. It now runs on every startup.
|
||||
"""
|
||||
db_path = tmp_path / "legacy_state.db"
|
||||
conn = sqlite3.connect(db_path)
|
||||
conn.executescript(
|
||||
"""
|
||||
CREATE TABLE schema_version (version INTEGER);
|
||||
INSERT INTO schema_version VALUES (12);
|
||||
CREATE TABLE sessions (
|
||||
id TEXT PRIMARY KEY, source TEXT, started_at REAL, ended_at REAL,
|
||||
message_count INTEGER DEFAULT 0, tool_call_count INTEGER DEFAULT 0,
|
||||
title TEXT, parent_session_id TEXT, model_config TEXT
|
||||
);
|
||||
CREATE TABLE messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT,
|
||||
tool_call_id TEXT, tool_calls TEXT, tool_name TEXT,
|
||||
timestamp REAL NOT NULL, token_count INTEGER, finish_reason TEXT,
|
||||
reasoning TEXT, reasoning_content TEXT, reasoning_details TEXT,
|
||||
codex_reasoning_items TEXT, codex_message_items TEXT,
|
||||
platform_message_id TEXT, observed INTEGER DEFAULT 0
|
||||
);
|
||||
CREATE TABLE state_meta (key TEXT PRIMARY KEY, value TEXT);
|
||||
"""
|
||||
)
|
||||
# Default-less active column, as seen in the wild (#51646 PRAGMA).
|
||||
conn.execute("ALTER TABLE messages ADD COLUMN active INTEGER")
|
||||
conn.execute("ALTER TABLE messages ADD COLUMN compacted INTEGER DEFAULT 0")
|
||||
conn.execute(
|
||||
"INSERT INTO sessions (id, source, started_at) VALUES ('s1', 'discord', 1.0)"
|
||||
)
|
||||
# A row written by the pre-fix INSERT: active is NULL.
|
||||
conn.execute(
|
||||
"INSERT INTO messages (session_id, role, content, timestamp) "
|
||||
"VALUES ('s1', 'user', 'old hidden turn', 1.0)"
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
session_db = SessionDB(db_path=db_path)
|
||||
try:
|
||||
active = session_db._conn.execute(
|
||||
"SELECT active FROM messages WHERE content = 'old hidden turn'"
|
||||
).fetchone()[0]
|
||||
assert active == 1
|
||||
assert len(session_db.get_messages_as_conversation("s1")) == 1
|
||||
finally:
|
||||
session_db.close()
|
||||
|
||||
def test_append_message_accepts_explicit_timestamp(self, db):
|
||||
db.create_session(session_id="s1", source="telegram")
|
||||
event_ts = 1777383653.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue