Preserve system messages during context compaction
The context compactor computed split_point against convo_msgs (system messages filtered out) but applied it directly to session.history which includes the system messages. After compaction, the original system prompt was dropped and replaced by an off-by-N slice of the full history. This silently dropped the system prompt (preset, persona, RAG context) from every compacted session — the model would lose persona, RAG, and preset guidance on the next turn after a long conversation. The split in maybe_compact does: convo_msgs = [m for m in messages if m['role'] != 'system'] split_point = len(convo_msgs) // 2 so split_point is indexed against the system-stripped list. But the helper _update_session_history took (session, split_point, summary) and did session.history[split_point:]. session.history is the full list including the leading system messages, so this dropped the first system_msg_count messages. Fix: pass system_msg_count=len(system_msgs) into _update_session_history and use session.history[system_msg_count + split_point:] as the recent slice, with session.history[:system_msg_count] prepended to preserve persona/preset/RAG system messages. Validated: tests/test_compactor_data_loss.py both tests now pass (were failing). tests/test_context_compactor.py 12 pre-existing tests still pass. Symptom was: post-compaction history = [summary] + assistant_1 + user_2 + assistant_2 (system_A was lost). Co-authored-by: Ernest Hysa <ernest@example.com>
This commit is contained in:
@@ -321,8 +321,12 @@ async def maybe_compact(
|
||||
|
||||
compacted = system_msgs + [summary_msg] + recent
|
||||
|
||||
# Update session history to match
|
||||
_update_session_history(session, split_point, summary)
|
||||
# Update session history to match. Pass len(system_msgs) so the
|
||||
# recent_history slice in _update_session_history uses the correct
|
||||
# offset — session.history INCLUDES the system messages, but
|
||||
# split_point is indexed against convo_msgs which does NOT. Without
|
||||
# this, the slice drops the leading system message(s).
|
||||
_update_session_history(session, split_point, summary, system_msg_count=len(system_msgs))
|
||||
|
||||
new_used = estimate_tokens(compacted)
|
||||
logger.info(
|
||||
@@ -333,22 +337,34 @@ async def maybe_compact(
|
||||
return compacted, context_length, True
|
||||
|
||||
|
||||
def _update_session_history(session, split_point: int, summary: str):
|
||||
"""Update the in-memory session history after compaction."""
|
||||
def _update_session_history(session, split_point: int, summary: str,
|
||||
system_msg_count: int = 0):
|
||||
"""Update the in-memory session history after compaction.
|
||||
|
||||
`split_point` is the index in `convo_msgs` (system-stripped). The
|
||||
in-memory `session.history` includes leading system messages, so the
|
||||
actual recent-history slice starts at `system_msg_count + split_point`.
|
||||
Prepending `session.history[:system_msg_count]` to the new history
|
||||
preserves persona, preset, and RAG system messages that would
|
||||
otherwise be dropped.
|
||||
"""
|
||||
if not session or not hasattr(session, "history"):
|
||||
return
|
||||
|
||||
if split_point >= len(session.history):
|
||||
effective_split = system_msg_count + split_point
|
||||
if effective_split >= len(session.history):
|
||||
return
|
||||
|
||||
# Keep the recent messages, prepend summary
|
||||
recent_history = session.history[split_point:]
|
||||
# Keep the recent messages, prepend summary AND the leading system
|
||||
# messages so the system prompt survives compaction.
|
||||
system_prefix = list(session.history[:system_msg_count])
|
||||
recent_history = session.history[effective_split:]
|
||||
summary_msg = ChatMessage(
|
||||
role="system",
|
||||
content=f"[Conversation summary]\n{summary}",
|
||||
metadata={"compacted": True, "summarized_count": split_point},
|
||||
)
|
||||
new_history = [summary_msg] + recent_history
|
||||
new_history = system_prefix + [summary_msg] + recent_history
|
||||
try:
|
||||
from core import models as _core_models
|
||||
manager = getattr(_core_models, "_session_manager", None)
|
||||
|
||||
Reference in New Issue
Block a user