fix(webui): keep idle compaction out of session recency

This commit is contained in:
chengyongru
2026-06-30 23:38:32 +08:00
committed by Xubin Ren
parent bfbae5a7b3
commit 3403b87641
7 changed files with 145 additions and 25 deletions
+30 -5
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Callable, Coroutine
from loguru import logger
from nanobot.agent.memory import LAST_COMPACTED_AT_META
from nanobot.session.manager import Session, SessionManager
if TYPE_CHECKING:
@@ -26,13 +27,36 @@ class AutoCompact:
self._archiving: set[str] = set()
self._summaries: dict[str, tuple[str, datetime]] = {}
@staticmethod
def _timestamp(ts: datetime | str | None) -> float | None:
if not ts:
return None
if isinstance(ts, str):
try:
ts = datetime.fromisoformat(ts)
except ValueError:
return None
return ts.timestamp()
def _is_expired(self, ts: datetime | str | None,
now: datetime | None = None) -> bool:
if self._ttl <= 0 or not ts:
ts_epoch = self._timestamp(ts)
if self._ttl <= 0 or ts_epoch is None:
return False
if isinstance(ts, str):
ts = datetime.fromisoformat(ts)
return ((now or datetime.now()) - ts).total_seconds() >= self._ttl * 60
now_epoch = self._timestamp(now or datetime.now())
return now_epoch is not None and now_epoch - ts_epoch >= self._ttl * 60
def _compacted_after_activity(self, key: str, last_active: datetime | str | None) -> bool:
metadata_row = self.sessions.read_session_metadata(key)
metadata = metadata_row.get("metadata") if isinstance(metadata_row, dict) else None
if not isinstance(metadata, dict):
return False
compacted_at = metadata.get(LAST_COMPACTED_AT_META)
if not isinstance(compacted_at, str):
return False
compacted_epoch = self._timestamp(compacted_at)
active_epoch = self._timestamp(last_active)
return compacted_epoch is not None and active_epoch is not None and compacted_epoch >= active_epoch
@staticmethod
def _format_summary(text: str, last_active: datetime) -> str:
@@ -52,7 +76,8 @@ class AutoCompact:
continue
if key in active_session_keys:
continue
if self._is_expired(info.get("updated_at"), now):
updated_at = info.get("updated_at")
if self._is_expired(updated_at, now) and not self._compacted_after_activity(key, updated_at):
self._archiving.add(key)
schedule_background(self._archive(key))
+6 -3
View File
@@ -33,6 +33,8 @@ if TYPE_CHECKING:
from nanobot.providers.base import LLMProvider
from nanobot.session.manager import SessionManager
LAST_COMPACTED_AT_META = "_last_compacted_at"
# ---------------------------------------------------------------------------
# MemoryStore — pure file I/O layer
@@ -1003,10 +1005,11 @@ class Consolidator:
async with lock:
self.sessions.invalidate(session_key)
session = self.sessions.get_or_create(session_key)
compacted_at = datetime.now().isoformat()
messages_to_summarize = list(session.messages[session.last_consolidated:])
if not messages_to_summarize:
session.updated_at = datetime.now()
session.metadata[LAST_COMPACTED_AT_META] = compacted_at
self.sessions.save(session)
return ""
@@ -1023,7 +1026,7 @@ class Consolidator:
messages_to_remove = result.dropped[result.already_consolidated_count:]
if not messages_to_remove and not messages_to_keep:
session.updated_at = datetime.now()
session.metadata[LAST_COMPACTED_AT_META] = compacted_at
self.sessions.save(session)
return ""
@@ -1046,7 +1049,7 @@ class Consolidator:
session.messages = messages_to_keep
session.last_consolidated = 0
session.updated_at = datetime.now()
session.metadata[LAST_COMPACTED_AT_META] = compacted_at
self.sessions.save(session)
if messages_to_remove:
+42 -6
View File
@@ -26,10 +26,11 @@ from nanobot.session.manager import (
_metadata_title,
)
_INDEX_VERSION = 1
_INDEX_VERSION = 2
_INDEX_FILENAME = ".webui_session_index.json"
_WEBUI_ACTIVITY_MTIME_NS = "webui_activity_mtime_ns"
_WEBUI_ACTIVITY_SIZE = "webui_activity_size"
_MESSAGE_ACTIVITY_ROLES = {"user", "assistant"}
def list_webui_sessions(session_manager: SessionManager) -> list[dict[str, Any]]:
@@ -214,14 +215,40 @@ def _latest_updated_at(stored: str | None, activity: str | None) -> str | None:
return stored
def _message_activity_updated_at(messages: list[dict[str, Any]]) -> str | None:
latest: str | None = None
for item in messages:
if item.get(CRON_HISTORY_META) is True:
continue
if item.get("role") not in _MESSAGE_ACTIVITY_ROLES:
continue
timestamp = item.get("timestamp")
if isinstance(timestamp, str):
latest = _latest_updated_at(latest, timestamp)
return latest
def _session_activity_updated_at(
stored: str | None,
message_activity: str | None,
webui_activity: str | None,
) -> str | None:
return _latest_updated_at(message_activity, webui_activity) or stored
def _indexed_row_for_session(session: Session, path: Path) -> dict[str, Any]:
signature = _file_signature(path)
activity_signature = _webui_activity_signature(session.key)
activity_updated_at = _webui_activity_updated_at(activity_signature)
message_updated_at = _message_activity_updated_at(session.messages)
return {
"key": session.key,
"created_at": session.created_at.isoformat(),
"updated_at": _latest_updated_at(session.updated_at.isoformat(), activity_updated_at),
"updated_at": _session_activity_updated_at(
session.updated_at.isoformat(),
message_updated_at,
activity_updated_at,
),
"title": _metadata_title(session.metadata),
"preview": _preview_from_messages(session.messages),
"file": path.name,
@@ -244,6 +271,7 @@ def _scan_session_row(session_manager: SessionManager, path: Path) -> dict[str,
return None
preview = ""
fallback_preview = ""
message_updated_at = None
scanned_records = 0
scanned_chars = 0
for line in f:
@@ -251,13 +279,17 @@ def _scan_session_row(session_manager: SessionManager, path: Path) -> dict[str,
continue
scanned_records += 1
scanned_chars += len(line)
item = json.loads(line)
if item.get("_type") == "metadata":
continue
if item.get(CRON_HISTORY_META) is not True and item.get("role") in _MESSAGE_ACTIVITY_ROLES:
timestamp = item.get("timestamp")
if isinstance(timestamp, str):
message_updated_at = _latest_updated_at(message_updated_at, timestamp)
if (
scanned_records > _SESSION_LIST_PREVIEW_MAX_RECORDS
or scanned_chars > _SESSION_LIST_PREVIEW_MAX_CHARS
):
break
item = json.loads(line)
if item.get("_type") == "metadata":
continue
if item.get(CRON_HISTORY_META) is True:
continue
@@ -282,7 +314,11 @@ def _scan_session_row(session_manager: SessionManager, path: Path) -> dict[str,
return {
"key": key,
"created_at": created_at_s,
"updated_at": _latest_updated_at(updated_at_s, activity_updated_at),
"updated_at": _session_activity_updated_at(
updated_at_s,
message_updated_at,
activity_updated_at,
),
"title": _metadata_title(data.get("metadata", {})),
"preview": preview or fallback_preview,
"file": path.name,