Merge pull request #4403 from nesquena/stage-4399

Release v0.51.486 — QV (archived cron runs stay hidden on state.db re-projection #4397)
This commit is contained in:
nesquena-hermes 2026-06-17 20:45:50 -07:00 committed by GitHub
commit 5ced430628
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 90 additions and 17 deletions

View file

@ -3,6 +3,12 @@
## [Unreleased]
## [v0.51.486] — 2026-06-18 — Release QV (archived cron runs stay hidden on state.db re-projection)
### Fixed
- **Archived cron runs no longer reappear in the Sessions panel after a refresh (#4397).** A follow-up to #4385/#4388: that fix stopped stale cron *sidecars* from being treated as CLI rows, but a remaining path let `get_cli_sessions()` re-project the same cron run straight from Hermes `state.db` with `archived: false` (after `all_sessions()` omitted the hidden sidecar). The state.db projection now reads the UI-owned sidecar metadata (title **and** archived flag) via a shared helper and carries the archived flag onto the projected row, so an archived cron/tool/API run stays hidden no matter which path surfaces it. Thanks @TomBanksAU.
## [v0.51.485] — 2026-06-18 — Release QU (mobile: bigger hamburger tap target + browser edge-swipe)
### Improved

View file

@ -3877,6 +3877,22 @@ def _all_profiles_cli_contexts() -> tuple[list[tuple[Path, Path, str | None]], t
return contexts, tuple(cache_entries)
def _state_projection_sidecar_metadata(sid: str) -> dict:
"""Return UI-owned metadata for a state.db-projected sidebar row."""
metadata = {"title": None, "archived": False}
try:
webui_meta = Session.load_metadata_only(sid)
except Exception:
return metadata
if not webui_meta:
return metadata
title = getattr(webui_meta, 'title', None)
if title:
metadata["title"] = title
metadata["archived"] = bool(getattr(webui_meta, 'archived', False))
return metadata
def _load_cli_sessions_uncached(
hermes_home: Path,
db_path: Path,
@ -3944,15 +3960,14 @@ def _load_cli_sessions_uncached(
except Exception:
pass # degrade gracefully
# If a WebUI JSON file exists for this session (e.g. previously
# imported or renamed in the sidebar), prefer its title over the
# state.db title. This fixes rename-not-persisting for CLI sessions
# after compression chain extension (#1486).
try:
_webui_meta = Session.load_metadata_only(sid)
if _webui_meta and getattr(_webui_meta, 'title', None):
_title = _webui_meta.title
except Exception:
pass
# imported or renamed in the sidebar), prefer its UI-owned metadata over
# the state.db projection. This keeps archived cron/tool/API runs hidden
# even when all_sessions() omits the hidden sidecar and the state row is
# re-injected from Hermes state.db (#4397).
_sidecar_meta = _state_projection_sidecar_metadata(sid)
if _sidecar_meta.get('title'):
_title = _sidecar_meta['title']
_archived = bool(_sidecar_meta.get('archived'))
_display_title = _title or f'{_source.title()} Session'
cli_sessions.append({
'session_id': sid,
@ -3963,7 +3978,7 @@ def _load_cli_sessions_uncached(
'created_at': row['started_at'],
'updated_at': raw_ts,
'pinned': False,
'archived': False,
'archived': _archived,
'project_id': _cron_pid() if is_cron_session(sid, _source) else None,
'profile': profile,
'source_tag': _source,
@ -4033,12 +4048,10 @@ def _load_cli_sessions_uncached(
break
except Exception:
pass
try:
_webui_meta = Session.load_metadata_only(sid)
if _webui_meta and getattr(_webui_meta, 'title', None):
_title = _webui_meta.title
except Exception:
pass
_sidecar_meta = _state_projection_sidecar_metadata(sid)
if _sidecar_meta.get('title'):
_title = _sidecar_meta['title']
_archived = bool(_sidecar_meta.get('archived'))
_display_title = _title or 'Cron Session'
cli_sessions.append({
'session_id': sid,
@ -4049,7 +4062,7 @@ def _load_cli_sessions_uncached(
'created_at': row['started_at'],
'updated_at': raw_ts,
'pinned': False,
'archived': False,
'archived': _archived,
'project_id': _cron_pid(),
'profile': profile_value,
'source_tag': 'cron',

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import sqlite3
from unittest.mock import patch
@ -79,6 +80,59 @@ def test_materializing_cron_session_preserves_non_cli_identity(monkeypatch):
assert session.is_cli_session is False
def test_cron_state_projection_preserves_archived_sidecar(monkeypatch, tmp_path):
"""A hidden archived sidecar must still mark the state.db cron projection archived."""
import api.models as models
sid = "cron_job123_20260618"
db_path = tmp_path / "state.db"
with sqlite3.connect(db_path) as conn:
conn.execute(
"""
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
title TEXT,
model TEXT,
message_count INTEGER,
started_at REAL,
source TEXT,
session_source TEXT
)
"""
)
conn.execute(
"""
INSERT INTO sessions (
id, title, model, message_count, started_at, source, session_source
) VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(sid, "Cron Session", "test-model", 1, 20, "cron", "cron"),
)
class ArchivedSidecar:
title = "Cron Session"
archived = True
monkeypatch.setattr(
models.Session,
"load_metadata_only",
staticmethod(lambda candidate: ArchivedSidecar() if candidate == sid else None),
)
monkeypatch.setattr(models, "ensure_cron_project", lambda: "cron-project")
rows = models._load_cli_sessions_uncached(
tmp_path,
db_path,
"default",
source_filter="cron",
include_claude_code=False,
)
assert len(rows) == 1
assert rows[0]["session_id"] == sid
assert rows[0]["archived"] is True
def test_archived_cron_sidecar_suppresses_raw_unarchived_cron_row(monkeypatch):
"""An archived cron sidecar should keep the raw state.db cron row hidden."""
import api.routes as routes