diff --git a/hermes_cli/main.py b/hermes_cli/main.py index bb5eef2ca4..0793b6a89e 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -596,32 +596,15 @@ def _session_browse_picker(sessions: list) -> Optional[str]: def _resolve_last_session(source: str = "cli") -> Optional[str]: - """Look up the most recently *used* session ID for a source. - - Previously this returned the most recently *started* session, which meant - `hermes -c` could skip the session you just closed if a newer one had been - opened earlier in a different window. We now order by last_active - (max message timestamp, falling back to started_at) so -c always resumes - the most recent conversation you actually touched. - """ + """Look up the most recent session ID for a source.""" try: from hermes_state import SessionDB db = SessionDB() - sessions = db.search_sessions(source=source, limit=20) + sessions = db.search_sessions(source=source, limit=1) db.close() - if not sessions: - return None - - def _last_active(s: dict) -> float: - v = s.get("last_active") or s.get("started_at") or 0 - try: - return float(v) - except (TypeError, ValueError): - return 0.0 - - sessions.sort(key=_last_active, reverse=True) - return sessions[0]["id"] + if sessions: + return sessions[0]["id"] except Exception: pass return None diff --git a/hermes_state.py b/hermes_state.py index b06d548bbc..30f94173e5 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1481,30 +1481,16 @@ class SessionDB: limit: int = 20, offset: int = 0, ) -> List[Dict[str, Any]]: - """List sessions, optionally filtered by source. - - Returns rows enriched with a computed ``last_active`` column (the - latest message timestamp for the session, falling back to - ``started_at``) so callers can sort by "most recently used" instead - of "most recently started". - """ - select_last_active = ( - "COALESCE(" - "(SELECT MAX(m.timestamp) FROM messages m WHERE m.session_id = s.id)," - " s.started_at" - ") AS last_active" - ) + """List sessions, optionally filtered by source.""" with self._lock: if source: cursor = self._conn.execute( - f"SELECT s.*, {select_last_active} FROM sessions s " - "WHERE s.source = ? ORDER BY s.started_at DESC LIMIT ? OFFSET ?", + "SELECT * FROM sessions WHERE source = ? ORDER BY started_at DESC LIMIT ? OFFSET ?", (source, limit, offset), ) else: cursor = self._conn.execute( - f"SELECT s.*, {select_last_active} FROM sessions s " - "ORDER BY s.started_at DESC LIMIT ? OFFSET ?", + "SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?", (limit, offset), ) return [dict(row) for row in cursor.fetchall()] diff --git a/tests/hermes_cli/test_resolve_last_session.py b/tests/hermes_cli/test_resolve_last_session.py deleted file mode 100644 index db4d321c11..0000000000 --- a/tests/hermes_cli/test_resolve_last_session.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Verify `hermes -c` picks the session the user most recently used.""" - -from __future__ import annotations - -from hermes_cli.main import _resolve_last_session - - -class _FakeDB: - def __init__(self, rows): - self._rows = rows - self.closed = False - - def search_sessions(self, source=None, limit=20, **_kw): - rows = [r for r in self._rows if r.get("source") == source] if source else list(self._rows) - return rows[:limit] - - def close(self): - self.closed = True - - -def test_resolve_last_session_prefers_last_active_over_started_at(monkeypatch): - # `search_sessions` returns in started_at DESC order, but the most recently - # *touched* session may have been started earlier. -c should pick by - # last_active so closing the active session and typing `hermes -c` resumes - # that one, not an older-but-newer-started session from another window. - rows = [ - { - "id": "new_started_old_active", - "source": "cli", - "started_at": 1000.0, - "last_active": 100.0, - }, - { - "id": "old_started_recently_active", - "source": "cli", - "started_at": 500.0, - "last_active": 999.0, - }, - ] - - fake_db = _FakeDB(rows) - monkeypatch.setattr("hermes_state.SessionDB", lambda: fake_db) - - assert _resolve_last_session("cli") == "old_started_recently_active" - assert fake_db.closed - - -def test_search_sessions_exposes_last_active_column(tmp_path, monkeypatch): - # End-to-end: the actual SessionDB must surface a last_active column so - # _resolve_last_session's sort works. A previous bug had last_active=None - # on every row because search_sessions used `SELECT *` with no computed - # column, silently breaking the -c resume behavior. - monkeypatch.setenv("HERMES_HOME", str(tmp_path)) - monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path) - - import hermes_state - - from pathlib import Path - - db = hermes_state.SessionDB(db_path=Path(tmp_path / "state.db")) - try: - db.create_session("s_started_later", source="cli") - db.create_session("s_active_later", source="cli") - # Force started_at ordering so the test is deterministic regardless - # of how quickly the two inserts land. - with db._lock: - db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (2000.0, "s_started_later")) - db._conn.execute("UPDATE sessions SET started_at=? WHERE id=?", (1000.0, "s_active_later")) - db._conn.commit() - - db.append_message("s_active_later", role="user", content="hi") - with db._lock: - db._conn.execute( - "UPDATE messages SET timestamp=? WHERE session_id=?", - (3000.0, "s_active_later"), - ) - db._conn.commit() - - rows = db.search_sessions(source="cli", limit=5) - ids = {r["id"]: r.get("last_active") for r in rows} - - assert ids["s_started_later"] == 2000.0 - assert ids["s_active_later"] == 3000.0 - finally: - db.close() - - -def test_resolve_last_session_returns_none_when_empty(monkeypatch): - monkeypatch.setattr("hermes_state.SessionDB", lambda: _FakeDB([])) - assert _resolve_last_session("cli") is None - - -def test_resolve_last_session_falls_back_to_started_at(monkeypatch): - # When last_active is missing entirely (legacy row), fall back to - # started_at so the helper still picks the newest session. - rows = [ - {"id": "older", "source": "cli", "started_at": 10.0}, - {"id": "newer", "source": "cli", "started_at": 20.0}, - ] - monkeypatch.setattr("hermes_state.SessionDB", lambda: _FakeDB(rows)) - assert _resolve_last_session("cli") == "newer"