From cb7cfba6ded3b071be74d3218d96741f12c7e56b Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 26 Apr 2026 16:21:57 -0500 Subject: [PATCH] fix(cli): surface last_active in search_sessions so -c works --- hermes_state.py | 20 ++++++++-- tests/hermes_cli/test_resolve_last_session.py | 40 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/hermes_state.py b/hermes_state.py index 3e5914c551..e92d5a3035 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1483,16 +1483,30 @@ class SessionDB: limit: int = 20, offset: int = 0, ) -> List[Dict[str, Any]]: - """List sessions, optionally filtered by source.""" + """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" + ) with self._lock: if source: cursor = self._conn.execute( - "SELECT * FROM sessions WHERE source = ? ORDER BY started_at DESC LIMIT ? OFFSET ?", + f"SELECT s.*, {select_last_active} FROM sessions s " + "WHERE s.source = ? ORDER BY s.started_at DESC LIMIT ? OFFSET ?", (source, limit, offset), ) else: cursor = self._conn.execute( - "SELECT * FROM sessions ORDER BY started_at DESC LIMIT ? OFFSET ?", + f"SELECT s.*, {select_last_active} FROM sessions s " + "ORDER BY s.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 index 68abc3df50..db4d321c11 100644 --- a/tests/hermes_cli/test_resolve_last_session.py +++ b/tests/hermes_cli/test_resolve_last_session.py @@ -45,6 +45,46 @@ def test_resolve_last_session_prefers_last_active_over_started_at(monkeypatch): 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