fix(session): prevent duplicate archive and message loss in enforce_file_cap

When retain_recent_legal_suffix hits the else branch (tail has no user
messages), it takes a non-contiguous slice from the middle of the session.
enforce_file_cap incorrectly assumed dropped messages were always a prefix
(before[:dropped_count]), causing user messages to be both archived and
retained, and some messages to silently disappear.

Fix by having retain_recent_legal_suffix return the actual dropped message
list using identity-based diff, so enforce_file_cap no longer needs to
guess which messages were removed.
This commit is contained in:
yorkhellen
2026-06-01 16:07:08 +08:00
committed by Xubin Ren
parent b886b4a566
commit 72fb642ef7
2 changed files with 124 additions and 14 deletions
+22 -14
View File
@@ -269,13 +269,19 @@ class Session:
self.updated_at = datetime.now()
self.metadata.pop("_last_summary", None)
def retain_recent_legal_suffix(self, max_messages: int) -> None:
"""Keep a legal recent suffix constrained by a hard message cap."""
def retain_recent_legal_suffix(self, max_messages: int) -> list[dict]:
"""Keep a legal recent suffix constrained by a hard message cap.
Returns the list of messages that were removed.
"""
if max_messages <= 0:
dropped = list(self.messages)
self.clear()
return
return dropped
if len(self.messages) <= max_messages:
return
return []
original = list(self.messages)
retained = list(self.messages[-max_messages:])
@@ -306,10 +312,16 @@ class Session:
if start:
retained = retained[start:]
dropped = len(self.messages) - len(retained)
# Compute actually-dropped messages using identity comparison so that
# even when retained is a non-contiguous slice of original (the else
# branch above), we never duplicate or lose messages.
retained_ids = set(id(m) for m in retained)
dropped = [m for m in original if id(m) not in retained_ids]
self.messages = retained
self.last_consolidated = max(0, self.last_consolidated - dropped)
self.last_consolidated = max(0, self.last_consolidated - len(dropped))
self.updated_at = datetime.now()
return dropped
def enforce_file_cap(
self,
@@ -320,23 +332,19 @@ class Session:
if limit <= 0 or len(self.messages) <= limit:
return
before = list(self.messages)
before_last_consolidated = self.last_consolidated
before_count = len(before)
self.retain_recent_legal_suffix(limit)
dropped_count = before_count - len(self.messages)
if dropped_count <= 0:
dropped = self.retain_recent_legal_suffix(limit)
if not dropped:
return
dropped = before[:dropped_count]
already_consolidated = min(before_last_consolidated, dropped_count)
already_consolidated = min(before_last_consolidated, len(dropped))
archive_chunk = dropped[already_consolidated:]
if archive_chunk and on_archive:
on_archive(archive_chunk)
logger.info(
"Session file cap hit for {}: dropped {}, raw-archived {}, kept {}",
self.key,
dropped_count,
len(dropped),
len(archive_chunk),
len(self.messages),
)