fix(sessions): bulk archive never hides a live chat because its old history matched
`hermes sessions archive --older-than N` (SessionDB.archive_sessions) selected every ENDED row matching the filters, which includes the compression ancestors of a long conversation: they are ended (`end_reason='compression'`) and old by construction. `set_session_archived` then flipped `archived` across the whole lineage — including the OPEN, actively written, lease-holding live tip — and the default `archived=exclude` listing (Desktop sidebar, `hermes sessions list`, /resume) lost the chat while messages kept flowing. A lineage is now matched through its tip only: `_prune_filter_where` gains `lineage_tips_only`, which `archive_sessions` always sets and the CLI sets in archive mode so the dry-run preview and the confirmation count show exactly the rows the archive will touch. An idle, ended tip still archives its whole chain (the lineage stays one unit — the listed row is the root, projected to the tip, so sparing only the tip would leave the chat hidden anyway). Prune is unchanged. Docs: the bulk-archive section says how compacted conversations are matched. Direction from #115500 by @whyyagswhy (automatic archives must not hide the open live tip); the mechanism differs because the listing keys on the root. Co-authored-by: whyyagswhy <166958865+whyyagswhy@users.noreply.github.com>
This commit is contained in:
@@ -72,7 +72,7 @@ _PRUNE_FILTERS = (
|
||||
("min_tool_calls", "notnone", _one("COALESCE(s.tool_call_count, 0) >= ?")),
|
||||
("max_tool_calls", "notnone", _one("COALESCE(s.tool_call_count, 0) <= ?")),
|
||||
)
|
||||
_PRUNE_FILTER_NAMES = frozenset(name for name, _, _ in _PRUNE_FILTERS) | {"archived", "include_pinned"}
|
||||
_PRUNE_FILTER_NAMES = frozenset(name for name, _, _ in _PRUNE_FILTERS) | {"archived", "include_pinned", "lineage_tips_only"}
|
||||
|
||||
|
||||
class SessionMaintenanceMixin:
|
||||
@@ -176,15 +176,20 @@ class SessionMaintenanceMixin:
|
||||
|
||||
@staticmethod
|
||||
def _prune_filter_where(*, archived: Optional[bool] = None, include_pinned: bool = False,
|
||||
**filters) -> Tuple[str, list]:
|
||||
lineage_tips_only: bool = False, **filters) -> Tuple[str, list]:
|
||||
"""Shared WHERE clause for bulk prune/archive selection (alias ``s``): ``_PRUNE_FILTERS``
|
||||
AND together, only ended sessions are ever candidates, ``archived`` is tri-state
|
||||
(None = both), ``*_like`` are case-insensitive substrings, the rest exact."""
|
||||
(None = both), ``*_like`` are case-insensitive substrings, the rest exact.
|
||||
``lineage_tips_only`` (bulk archive) drops compression ancestors: they are archived with
|
||||
their tip, never on their own age — matching an old ancestor would fan out over the lineage
|
||||
and hide its OPEN, recently active tip (#115489)."""
|
||||
unknown = set(filters) - _PRUNE_FILTER_NAMES
|
||||
if unknown:
|
||||
raise TypeError("SessionMaintenanceMixin._prune_filter_where() got an unexpected "
|
||||
f"keyword argument {sorted(unknown)[0]!r}")
|
||||
clauses = ["s.ended_at IS NOT NULL"]
|
||||
if lineage_tips_only:
|
||||
clauses.append("COALESCE(s.end_reason, '') <> 'compression'")
|
||||
params: list = []
|
||||
for name, applies, build in _PRUNE_FILTERS:
|
||||
value = filters.get(name)
|
||||
|
||||
Reference in New Issue
Block a user