Description
User messages from platform adapters (confirmed on Feishu, likely affects all platforms) are stored twice in the messages table of state.db. One entry has platform_message_id, the other doesn't.
Root Cause
There are two independent write paths using two different SessionDB instances that both connect to the same SQLite file:
-
Agent's _flush_messages_to_session_db() (run_agent.py:1533)
- Called during
run_conversation() via _post_run_persist (line 1473)
- Uses the
SessionDB instance passed from GatewayRunner._session_db (line 13804)
- Does not include
platform_message_id in the write
- Writes first (during agent execution)
-
Gateway's session_store.append_to_transcript() (gateway/run.py:8899)
- Called after
run_conversation() returns
- Uses
SessionStore._db — a separate SessionDB instance (created at session.py:705)
- Includes
platform_message_id from event.message_id
- Writes second (after agent returns)
The skip_db mechanism
There is a skip_db mechanism at line 8879-8901 designed to prevent this:
agent_persisted = self._session_db is not None
# ...
self.session_store.append_to_transcript(
session_entry.session_id, entry,
skip_db=agent_persisted,
)
This works for the normal path (else branch at line 8874). However, there are two fallback paths that completely bypass skip_db:
# Path 1: agent_failed_early (line 8844-8855) — NO skip_db
_user_entry = {"role": "user", "content": message_text, "timestamp": ts}
if event.message_id:
_user_entry["message_id"] = str(event.message_id)
self.session_store.append_to_transcript(session_entry.session_id, _user_entry)
# Path 2: not new_messages fallback (line 8861-8873) — NO skip_db
if not new_messages:
_user_entry = {"role": "user", "content": message_text, "timestamp": ts}
if event.message_id:
_user_entry["message_id"] = str(event.message_id)
self.session_store.append_to_transcript(session_entry.session_id, _user_entry)
When either fallback triggers, the gateway writes the user message unconditionally, regardless of whether the agent already persisted it.
Reproduction
- Send a message via any platform (confirmed with Feishu)
- Query
state.db for the session's messages:
SELECT id, role, content, platform_message_id, timestamp
FROM messages
WHERE session_id = '<session_id>' AND role = 'user'
ORDER BY timestamp;
- Observe duplicate user entries — one with
platform_message_id, one without.
Evidence
From session 20260608_155640_bba43481:
id=32737 role=user ts=1780914776.291 platform_message_id=None content="明白了"
id=32738 role=user ts=1780914781.082 platform_message_id=om_xxx content="明白了"
id=32679 role=user ts=1780912724.451 platform_message_id=None content="是的"
id=32680 role=user ts=1780912736.180 platform_message_id=om_xxx content="是的"
id=32657 role=user ts=1780911959.371 platform_message_id=None content="嗯呢"
id=32658 role=user ts=1780911974.459 platform_message_id=om_xxx content="嗯呢"
Pattern: 7 duplicate pairs in this session alone. The entry without platform_message_id always comes first (from agent), the one with it comes 5-24 seconds later (from gateway).
Suggested Fix
Add skip_db to both fallback paths in gateway/run.py:
# Fix for agent_failed_early path (line 8844)
agent_persisted = self._session_db is not None
self.session_store.append_to_transcript(
session_entry.session_id, _user_entry,
skip_db=agent_persisted,
)
# Fix for not new_messages fallback (line 8861)
agent_persisted = self._session_db is not None
self.session_store.append_to_transcript(
session_entry.session_id, _user_entry,
skip_db=agent_persisted,
)
if response:
self.session_store.append_to_transcript(
session_entry.session_id,
{"role": "assistant", "content": response, "timestamp": ts},
skip_db=agent_persisted,
)
Environment
- Hermes Agent version: fork of
NousResearch/hermes-agent (synced with main as of 2026-06-08)
- Platform: Feishu (likely affects all platforms)
- Model: xiaomi/mimo-v2.5-pro
- OS: Ubuntu 24.04
Description
User messages from platform adapters (confirmed on Feishu, likely affects all platforms) are stored twice in the
messagestable ofstate.db. One entry hasplatform_message_id, the other doesn't.Root Cause
There are two independent write paths using two different
SessionDBinstances that both connect to the same SQLite file:Agent's
_flush_messages_to_session_db()(run_agent.py:1533)run_conversation()via_post_run_persist(line 1473)SessionDBinstance passed fromGatewayRunner._session_db(line 13804)platform_message_idin the writeGateway's
session_store.append_to_transcript()(gateway/run.py:8899)run_conversation()returnsSessionStore._db— a separateSessionDBinstance (created atsession.py:705)platform_message_idfromevent.message_idThe skip_db mechanism
There is a
skip_dbmechanism at line 8879-8901 designed to prevent this:This works for the normal path (
elsebranch at line 8874). However, there are two fallback paths that completely bypassskip_db:When either fallback triggers, the gateway writes the user message unconditionally, regardless of whether the agent already persisted it.
Reproduction
state.dbfor the session's messages:platform_message_id, one without.Evidence
From session
20260608_155640_bba43481:Pattern: 7 duplicate pairs in this session alone. The entry without
platform_message_idalways comes first (from agent), the one with it comes 5-24 seconds later (from gateway).Suggested Fix
Add
skip_dbto both fallback paths ingateway/run.py:Environment
NousResearch/hermes-agent(synced with main as of 2026-06-08)