mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
fix: prevent state.db messages being silently dropped during sidecar merge
Two bugs combined to cause historical messages to vanish from the WebUI
after a session was continued in a later conversation.
**Bug 1 — missing `id` in state.db SELECT (models.py)**
`get_state_db_session_messages()` did not include the `id` column in its
SELECT, so every row got a `("legacy", ...)` merge key instead of
`("message_id", ...)`. The timestamp gate in
`merge_session_messages_append_only()` explicitly exempts `message_id`-keyed
rows from its "skip if older than newest sidecar message" rule, but
legacy-keyed rows are unconditionally dropped. With a session that has any
new sidecar messages (max_sidecar_timestamp == today), all older state.db
rows were silently discarded.
Fix: include `id` when the column is present so rows get proper
`("message_id", ...)` keys and survive the timestamp filter.
**Bug 2 — always reads active profile's state.db, not the session's (models.py + routes.py)**
`get_state_db_session_messages()` always called `_active_state_db_path()`,
which returns the currently-active profile's database. Sessions belonging to
a different profile (e.g. `jump`) were read from the wrong state.db, returning
either no rows or unrelated ones.
Fix: add an optional `profile` parameter; when supplied, resolve the path via
`_get_profile_home(profile)` with a fallback to the active path if the
profile-specific db does not exist. The call-site in `routes.py` now reads
`session.profile` and passes it through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
nesquena-hermes
parent
465b97a9f5
commit
ee672df463
+17
-9
@@ -2815,21 +2815,28 @@ def _json_loads_if_string(value):
|
||||
return value
|
||||
|
||||
|
||||
def get_state_db_session_messages(sid, *, stitch_continuations: bool = False) -> list:
|
||||
"""Read messages for a Hermes session from the active profile's state.db.
|
||||
def get_state_db_session_messages(sid, *, stitch_continuations: bool = False, profile=None) -> list:
|
||||
"""Read messages for a Hermes session from state.db.
|
||||
|
||||
This generic reader intentionally works for any session source, including
|
||||
WebUI-origin sessions that were later updated through another Hermes surface
|
||||
such as the Gateway API Server. When ``stitch_continuations`` is true it
|
||||
preserves the historical CLI/external-agent behavior of walking compatible
|
||||
compression/close parent segments before reading messages.
|
||||
When *profile* is supplied, reads from that profile's state.db; otherwise
|
||||
falls back to the active profile's state.db. This generic reader works for
|
||||
any session source, including WebUI-origin sessions that were later updated
|
||||
through another Hermes surface such as the Gateway API Server. When
|
||||
``stitch_continuations`` is true it preserves the historical CLI/external-agent
|
||||
behavior of walking compatible compression/close parent segments before reading
|
||||
messages.
|
||||
"""
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
db_path = _active_state_db_path()
|
||||
if isinstance(profile, str) and profile:
|
||||
db_path = _get_profile_home(profile) / 'state.db'
|
||||
if not db_path.exists():
|
||||
db_path = _active_state_db_path()
|
||||
else:
|
||||
db_path = _active_state_db_path()
|
||||
if not db_path.exists():
|
||||
return []
|
||||
|
||||
@@ -2852,7 +2859,8 @@ def get_state_db_session_messages(sid, *, stitch_continuations: bool = False) ->
|
||||
'reasoning_content',
|
||||
'codex_message_items',
|
||||
]
|
||||
selected = ['role', 'content', 'timestamp'] + [c for c in optional if c in available]
|
||||
id_col = ['id'] if 'id' in available else []
|
||||
selected = id_col + ['role', 'content', 'timestamp'] + [c for c in optional if c in available]
|
||||
|
||||
session_chain = [str(sid)]
|
||||
if stitch_continuations:
|
||||
|
||||
+3
-2
@@ -3752,17 +3752,18 @@ def handle_get(handler, parsed) -> bool:
|
||||
cli_messages = []
|
||||
state_db_messages = []
|
||||
sidecar_metadata_messages = None
|
||||
_session_profile = getattr(s, 'profile', None) or None
|
||||
if is_messaging_session:
|
||||
cli_messages = get_cli_session_messages(sid)
|
||||
elif load_messages:
|
||||
state_db_messages = get_state_db_session_messages(sid)
|
||||
state_db_messages = get_state_db_session_messages(sid, profile=_session_profile)
|
||||
elif not is_messaging_session:
|
||||
# Metadata-only callers still need the same append-only
|
||||
# reconciliation contract as full loads. A raw state.db summary
|
||||
# can count stale rows that the merge intentionally filters out,
|
||||
# which makes sidebar polling think the transcript is always
|
||||
# newer than the loaded conversation.
|
||||
state_db_messages = get_state_db_session_messages(sid)
|
||||
state_db_messages = get_state_db_session_messages(sid, profile=_session_profile)
|
||||
sidecar_metadata_session = Session.load(sid)
|
||||
sidecar_metadata_messages = (
|
||||
getattr(sidecar_metadata_session, "messages", []) or []
|
||||
|
||||
Reference in New Issue
Block a user