feat(desktop): search sessions by id
This commit is contained in:
+39
-11
@@ -1611,14 +1611,15 @@ async def get_sessions(
|
||||
|
||||
@app.get("/api/sessions/search")
|
||||
async def search_sessions(q: str = "", limit: int = 20):
|
||||
"""Full-text search across session message content using FTS5.
|
||||
"""Search sessions by ID plus full-text message content using FTS5.
|
||||
|
||||
Results are deduped by compression lineage, not by raw ``session_id``.
|
||||
Auto-compression rotates a conversation onto a fresh session id (and leaves
|
||||
the old segment's messages in the FTS index), so one logical chat can own
|
||||
many ``sessions`` rows that all match the same query. Branches also use
|
||||
``parent_session_id``, but they are real alternate conversations; don't
|
||||
collapse branch-specific hits back into the parent.
|
||||
Direct session-id matches are surfaced first, then FTS message-content
|
||||
matches. Results are deduped by compression lineage, not by raw
|
||||
``session_id``. Auto-compression rotates a conversation onto a fresh
|
||||
session id (and leaves the old segment's messages in the FTS index), so one
|
||||
logical chat can own many ``sessions`` rows that all match the same query.
|
||||
Branches also use ``parent_session_id``, but they are real alternate
|
||||
conversations; don't collapse branch-specific hits back into the parent.
|
||||
"""
|
||||
if not q or not q.strip():
|
||||
return {"results": []}
|
||||
@@ -1626,6 +1627,32 @@ async def search_sessions(q: str = "", limit: int = 20):
|
||||
from hermes_state import SessionDB
|
||||
db = SessionDB()
|
||||
try:
|
||||
safe_limit = max(1, min(int(limit or 20), 100))
|
||||
seen: dict = {}
|
||||
|
||||
def add_result(sid: str, payload: dict) -> None:
|
||||
if sid and sid not in seen and len(seen) < safe_limit:
|
||||
seen[sid] = payload
|
||||
|
||||
# Direct ID matches first: users often paste a session id from CLI,
|
||||
# logs, or another Hermes surface. FTS can't find those unless the
|
||||
# id happens to appear in message text.
|
||||
for row in db.search_sessions_by_id(q, limit=safe_limit, include_archived=True):
|
||||
sid = row.get("id")
|
||||
preview = (row.get("preview") or "").strip()
|
||||
snippet = preview or f"Session ID: {sid}"
|
||||
add_result(
|
||||
sid,
|
||||
{
|
||||
"session_id": sid,
|
||||
"snippet": snippet,
|
||||
"role": None,
|
||||
"source": row.get("source"),
|
||||
"model": row.get("model"),
|
||||
"session_started": row.get("started_at"),
|
||||
},
|
||||
)
|
||||
|
||||
# Auto-add prefix wildcards so partial words match
|
||||
# e.g. "nimb" → "nimb*" matches "nimby"
|
||||
# Preserve quoted phrases and existing wildcards as-is
|
||||
@@ -1639,7 +1666,7 @@ async def search_sessions(q: str = "", limit: int = 20):
|
||||
prefix_query = " ".join(terms)
|
||||
# Over-fetch so lineage dedup can still surface `limit` distinct
|
||||
# conversations even when several hits collapse onto one root.
|
||||
fetch_limit = max(limit * 5, 50)
|
||||
fetch_limit = max(safe_limit * 5, 50)
|
||||
matches = db.search_messages(query=prefix_query, limit=fetch_limit)
|
||||
|
||||
# Walk parent_session_id to the compression root, memoized so a
|
||||
@@ -1713,12 +1740,15 @@ async def search_sessions(q: str = "", limit: int = 20):
|
||||
return tip
|
||||
|
||||
# Keep the best (first / most relevant) hit per compression root.
|
||||
seen: dict = {}
|
||||
# `seen` already holds the direct ID matches collected above; the
|
||||
# content matches extend it without clobbering them.
|
||||
for m in matches:
|
||||
raw_sid = m["session_id"]
|
||||
root = compression_root(raw_sid)
|
||||
if root in seen:
|
||||
continue
|
||||
if len(seen) >= safe_limit:
|
||||
break
|
||||
seen[root] = {
|
||||
"session_id": lineage_tip(root),
|
||||
"lineage_root": root,
|
||||
@@ -1728,8 +1758,6 @@ async def search_sessions(q: str = "", limit: int = 20):
|
||||
"model": m.get("model"),
|
||||
"session_started": m.get("session_started"),
|
||||
}
|
||||
if len(seen) >= limit:
|
||||
break
|
||||
return {"results": list(seen.values())}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user