Summary
compress_context() in agent/conversation_compression.py rotates the session_id (end_session → create_session with parent_session_id) without calling _flush_messages_to_session_db() first. Messages generated during the current turn that haven't yet been persisted to state.db are permanently lost.
The same bug exists in cli.py:new_session() (the /new command, line 6442).
Root Cause
_persist_session() (which calls _flush_messages_to_session_db()) only runs at turn end — in conversation_loop.py exit paths. But auto-compression can trigger mid-turn when context exceeds the threshold. At that point:
compress_context() is called with the in-memory messages list
- The compressor summarizes old messages → produces a shorter
compressed list
end_session() is called on the old session_id — messages never written to DB
- A new session_id is created with
parent_session_id=old_session_id
_last_flushed_db_idx is reset to 0 for the new session
The result: the old session has message_count from its last clean turn, but ALL messages from the turn that triggered compression are gone.
Impact
Severity: High — permanent data loss, completely silent (no error, no warning).
- Any conversation turn that triggers auto-compression loses all messages from that turn
session_search returns incomplete results for compressed conversations
/resume produces a gap in conversation history
- No offline backup or recovery path for the lost messages
- Particularly painful for long tool-heavy turns (vision analysis, code execution, multi-step workflows) where 50+ messages can be generated in a single turn
Affected Code
Primary: agent/conversation_compression.py → compress_context()
Around lines 498–533, the agent._session_db block calls end_session() without first calling _flush_messages_to_session_db().
Call sites that trigger this (all in agent/conversation_loop.py):
- Line 653 — preflight compression
- Line 2660 — auto-compression during API loop
- Line 2834 — auto-compression during tool execution loop
- Line 2990 — auto-compression during tool execution loop
- Line 3913 — auto-compression during API retry loop
Same bug: cli.py:new_session() (line 6442)
The /new command calls end_session() without _flush_messages_to_session_db() — typing /new mid-turn also loses the current turn's unflushed messages.
Real-World Example
Session 20260601_061248_d68186d8 ("手寫積分筆記數位化"):
- Started with 70+ turns, 190+ tool turns
- Heavy work: vision analysis, code execution, P1 iteration (3 rounds), P2 step 1, skill creation
- At 08:44:
max_iterations_reached(16/16) — context compaction triggered
- Result: DB only has 26 messages (bootstrap + last ~2 turns). All P1/P2 content permanently lost.
Suggested Fix
In compress_context(), before agent._session_db.end_session():
# Flush in-memory messages to DB before ending the old session.
try:
agent._flush_messages_to_session_db(messages)
except Exception as _flush_err:
logger.warning("Failed to flush messages before compression: %s", _flush_err)
This is safe because _flush_messages_to_session_db uses _last_flushed_db_idx — it only writes genuinely new messages, never re-writes old ones. No LLM context is affected — pure local SQLite insert.
Apply the same fix to cli.py:new_session().
Discovered by
Hermes Agent (dogfooding session — investigated compaction data loss)
Reported from session: 20260601_061248_d68186d8
Related skill: session-compaction-data-loss (local)
Summary
compress_context()inagent/conversation_compression.pyrotates the session_id (end_session→create_sessionwithparent_session_id) without calling_flush_messages_to_session_db()first. Messages generated during the current turn that haven't yet been persisted tostate.dbare permanently lost.The same bug exists in
cli.py:new_session()(the/newcommand, line 6442).Root Cause
_persist_session()(which calls_flush_messages_to_session_db()) only runs at turn end — inconversation_loop.pyexit paths. But auto-compression can trigger mid-turn when context exceeds the threshold. At that point:compress_context()is called with the in-memorymessageslistcompressedlistend_session()is called on the old session_id — messages never written to DBparent_session_id=old_session_id_last_flushed_db_idxis reset to 0 for the new sessionThe result: the old session has
message_countfrom its last clean turn, but ALL messages from the turn that triggered compression are gone.Impact
Severity: High — permanent data loss, completely silent (no error, no warning).
session_searchreturns incomplete results for compressed conversations/resumeproduces a gap in conversation historyAffected Code
Primary:
agent/conversation_compression.py→compress_context()Around lines 498–533, the
agent._session_dbblock callsend_session()without first calling_flush_messages_to_session_db().Call sites that trigger this (all in
agent/conversation_loop.py):Same bug:
cli.py:new_session()(line 6442)The
/newcommand callsend_session()without_flush_messages_to_session_db()— typing/newmid-turn also loses the current turn's unflushed messages.Real-World Example
Session
20260601_061248_d68186d8("手寫積分筆記數位化"):max_iterations_reached(16/16)— context compaction triggeredSuggested Fix
In
compress_context(), beforeagent._session_db.end_session():This is safe because
_flush_messages_to_session_dbuses_last_flushed_db_idx— it only writes genuinely new messages, never re-writes old ones. No LLM context is affected — pure local SQLite insert.Apply the same fix to
cli.py:new_session().Discovered by
Hermes Agent (dogfooding session — investigated compaction data loss)
Reported from session:
20260601_061248_d68186d8Related skill:
session-compaction-data-loss(local)