diff --git a/hermes_cli/config_defaults.py b/hermes_cli/config_defaults.py index d6763c583d..e7d02a75d9 100644 --- a/hermes_cli/config_defaults.py +++ b/hermes_cli/config_defaults.py @@ -2086,15 +2086,15 @@ DEFAULT_CONFIG = { # Automatic cleanup of ~/.hermes/state.db, which otherwise grows without bound and slows FTS5 # inserts, /resume listing, and insights queries. "sessions": { - # Prune ENDED sessions inactive for retention_days (activity = latest message, else - # creation) about once per min_interval_hours at startup. Open, pinned, or mid-turn sessions + # Prune ENDED sessions inactive for retention_days (activity = freshest of live activity / + # latest message / creation) about once per min_interval_hours at startup. Open, pinned, or mid-turn sessions # are never deleted; stale automation sessions whose process died are *closed*, then get a # full retention window before removal. "auto_prune": True, # Inactive days of ended-session history to keep (= `hermes sessions prune`). # When true, prune ENDED sessions inactive for retention_days once per (roughly) min_interval_hours - # at CLI/gateway/cron startup. Activity is the latest message timestamp, falling back to creation - # time for empty sessions. Sessions that are still open, pinned, or mid-turn are never deleted — the + # at CLI/gateway/cron startup. Activity is the freshest of live activity (last_activity_at) / latest + # message timestamp / creation time. Sessions that are still open, pinned, or mid-turn are never deleted — the # only open rows the sweep touches are stale automation sessions (cron/kanban/subagent/one-shot CLI) # whose process died without closing them; those are *closed*, not deleted, and get a further full # retention window before removal. Default true since #54189: without it state.db grows without diff --git a/hermes_cli/session_filters.py b/hermes_cli/session_filters.py index ea8693dd6d..ef1649534f 100644 --- a/hermes_cli/session_filters.py +++ b/hermes_cli/session_filters.py @@ -96,8 +96,8 @@ _ARG_FILTERS = ( def build_prune_filters(args: Any) -> Dict[str, Any]: """Translate argparse Namespace flags into SessionDB filter kwargs. - ``--older-than`` / ``--newer-than`` bound last activity (latest message timestamp, falling back - to ``started_at`` for empty sessions); ``--before`` / ``--after`` bound session start time. + ``--older-than`` / ``--newer-than`` bound last activity (freshest of ``last_activity_at`` / + latest message / ``started_at``); ``--before`` / ``--after`` bound session start time. """ bounds: Dict[str, Optional[float]] = { key: None if (raw := getattr(args, attr, None)) is None else parse_point_in_time(raw, flag) diff --git a/hermes_state_maintenance.py b/hermes_state_maintenance.py index a870470cb0..71f038be1a 100644 --- a/hermes_state_maintenance.py +++ b/hermes_state_maintenance.py @@ -209,7 +209,7 @@ class SessionMaintenanceMixin: def list_prune_candidates(self, older_than_days: Optional[float] = None, source: str = None, **filters) -> List[Dict[str, Any]]: """Dry-run: sessions a matching prune/archive would touch, oldest first (``older_than_days`` - = inactivity threshold: latest message, else ``started_at``).""" + = inactivity threshold: freshest of ``last_activity_at`` / latest message / ``started_at``).""" where, params = self._prune_where(older_than_days, source, filters) return [dict(row) for row in self._read_all( f"""SELECT s.id, s.source, s.title, s.model, s.started_at, @@ -253,7 +253,7 @@ class SessionMaintenanceMixin: AND COALESCE(s.end_reason, '') <> 'compression' {pin_clause} AND NOT (COALESCE(s.hidden, 0) <> 0 AND COALESCE(s.title, '') = ?) - AND {_sql_session_last_active("s")} < ? + AND {_LAST_ACTIVE_SQL} < ? ORDER BY s.started_at ASC """, (self.CANONICAL_BOT_CHAT_TITLE, cutoff)) for row in rows: