fix(desktop): guard reconnect sockets and keep branch search precise

Avoid stale WebSocket events from an old reconnect attempt flipping the gateway state after a newer socket opens. Also limit session-search dedupe to compression edges so branch-specific hits still open the branch instead of collapsing to the parent.
This commit is contained in:
Brooklyn Nicholson
2026-06-03 13:13:21 -05:00
parent 93228d5299
commit 1b89715e15
3 changed files with 83 additions and 21 deletions
+30 -13
View File
@@ -1531,15 +1531,12 @@ async def get_sessions(
async def search_sessions(q: str = "", limit: int = 20):
"""Full-text search across session message content using FTS5.
Results are deduped by *conversation lineage*, not by raw ``session_id``.
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), and branches copy the
transcript into a new row so one logical chat owns many ``sessions`` rows
that all match the same query. Without lineage dedup the sidebar shows the
same conversation several times, which is the "multiple copies / branches"
navigation complaint. We collapse every match to its lineage root and
surface the live compression tip so clicking the result resumes the current
session.
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": []}
@@ -1563,11 +1560,13 @@ async def search_sessions(q: str = "", limit: int = 20):
fetch_limit = max(limit * 5, 50)
matches = db.search_messages(query=prefix_query, limit=fetch_limit)
# Walk parent_session_id to the lineage root, memoized so a chain of
# compression segments only costs one walk.
# Walk parent_session_id to the compression root, memoized so a
# chain of compression segments only costs one walk. We deliberately
# stop at branch/delegate edges: those sessions may diverge from the
# parent and should remain searchable on their own.
root_cache: dict = {}
def lineage_root(session_id: str) -> str:
def compression_root(session_id: str) -> str:
if not session_id:
return session_id
if session_id in root_cache:
@@ -1593,6 +1592,24 @@ async def search_sessions(q: str = "", limit: int = 20):
if not parent:
root = cur
break
try:
parent_session = db.get_session(parent)
except Exception:
parent_session = None
if not parent_session:
root = cur
break
parent_ended_at = parent_session.get("ended_at")
started_at = s.get("started_at")
is_compression_edge = (
parent_session.get("end_reason") == "compression"
and parent_ended_at is not None
and started_at is not None
and started_at >= parent_ended_at
)
if not is_compression_edge:
root = cur
break
cur = parent
for node in chain:
root_cache[node] = root
@@ -1613,11 +1630,11 @@ async def search_sessions(q: str = "", limit: int = 20):
tip_cache[root_id] = tip
return tip
# Keep the best (first / most relevant) hit per lineage root.
# Keep the best (first / most relevant) hit per compression root.
seen: dict = {}
for m in matches:
raw_sid = m["session_id"]
root = lineage_root(raw_sid)
root = compression_root(raw_sid)
if root in seen:
continue
seen[root] = {