diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 8c33a383e5..6298274d78 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -761,20 +761,32 @@ async def search_sessions(q: str = "", limit: int = 20): else: terms.append(token + "*") prefix_query = " ".join(terms) - matches = db.search_messages(query=prefix_query, limit=limit) - # Group by session_id — return unique sessions with their best snippet + # search_messages() limits raw message hits, but this endpoint returns + # unique sessions. Overfetch in batches until we either collect the + # requested number of unique sessions or exhaust the match list. seen: dict = {} - for m in matches: - sid = m["session_id"] - if sid not in seen: - seen[sid] = { - "session_id": sid, - "snippet": m.get("snippet", ""), - "role": m.get("role"), - "source": m.get("source"), - "model": m.get("model"), - "session_started": m.get("session_started"), - } + offset = 0 + batch_size = max(limit, 20) + while len(seen) < limit: + matches = db.search_messages(query=prefix_query, limit=batch_size, offset=offset) + if not matches: + break + for m in matches: + sid = m["session_id"] + if sid not in seen: + seen[sid] = { + "session_id": sid, + "snippet": m.get("snippet", ""), + "role": m.get("role"), + "source": m.get("source"), + "model": m.get("model"), + "session_started": m.get("session_started"), + } + if len(seen) >= limit: + break + if len(matches) < batch_size: + break + offset += batch_size return {"results": list(seen.values())} finally: db.close() diff --git a/hermes_state.py b/hermes_state.py index ed95d25f45..721b375a7a 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -1292,7 +1292,7 @@ class SessionDB: JOIN messages m ON m.id = messages_fts.rowid JOIN sessions s ON s.id = m.session_id WHERE {where_sql} - ORDER BY rank + ORDER BY rank, m.id LIMIT ? OFFSET ? """ diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index e83f5bdeb3..3f9c964eb1 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -817,6 +817,25 @@ class TestNewEndpoints: except Exception: pass + def test_session_search_returns_limit_unique_sessions(self): + from hermes_state import SessionDB + + db = SessionDB() + try: + db.create_session(session_id="session-a", source="cli", model="anthropic/claude-sonnet-4") + db.create_session(session_id="session-b", source="cli", model="anthropic/claude-sonnet-4") + for _ in range(25): + db.append_message("session-a", role="user", content="needle exact phrase") + db.append_message("session-b", role="user", content="needle exact phrase") + finally: + db.close() + + resp = self.client.get('/api/sessions/search?q="needle exact phrase"&limit=2') + + assert resp.status_code == 200 + data = resp.json() + assert [result["session_id"] for result in data["results"]] == ["session-a", "session-b"] + # --------------------------------------------------------------------------- # Model context length: normalize/denormalize + /api/model/info