perf(desktop): make session-id search SQL-bounded, not O(n)

search_sessions_by_id previously fetched up to 10k sessions via
list_sessions_rich and filtered them in Python — O(n) per keystroke.
Push the id match into SQL instead.

- list_sessions_rich gains an optional id_query param: a case-insensitive
  LIKE pushed into the outer WHERE, matched against each surfaced row's id
  AND every id in its forward compression chain (via the existing chain
  CTE). Searching a compression root id or a tip id both resolve to the
  same projected conversation. LIKE wildcards in the needle are escaped.
- search_sessions_by_id now fetches only matching rows (limit*4) and ranks
  exact > prefix > substring in Python over that small set.
- web_server /api/sessions/search: route ID matches and content matches
  through one lineage-keyed dedup helper so an id-hit and a content-hit on
  the same conversation collapse to a single result (the contributor's
  version keyed ID hits by raw sid and content hits by root, which could
  double-list a compression tip).
- command-center haystack also matches _lineage_root_id for parity.

E2E verified against a real DB: exact match over 3000+ sessions
materializes 1 row in Python (was ~3000), 5ms; root-id resolves to tip;
LIKE-wildcard escaping holds.

Follow-up to @0xharryriddle's feat(desktop): search sessions by id.
This commit is contained in:
Teknium
2026-06-04 07:49:34 -07:00
parent 9ecc331be8
commit 580d924097
4 changed files with 133 additions and 73 deletions
+51 -14
View File
@@ -1565,6 +1565,7 @@ class SessionDB:
order_by_last_active: bool = False,
include_archived: bool = False,
archived_only: bool = False,
id_query: str = None,
) -> List[Dict[str, Any]]:
"""List sessions with preview (first user message) and last active timestamp.
@@ -1626,6 +1627,16 @@ class SessionDB:
where_clauses.append("s.archived = 0")
where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
# Optional session-id filter, pushed into SQL so callers (Desktop
# session-id search) don't have to fetch every row and filter in
# Python. ``id_query`` is matched as a case-insensitive substring
# against each surfaced row's id AND every id in its forward
# compression chain — so searching a compression *root* id or a *tip*
# id both resolve to the same projected conversation. Only used in the
# order_by_last_active path (which builds the chain CTE); other callers
# pass id_query=None.
id_needle = (id_query or "").strip().lower()
if order_by_last_active:
# Compute effective_last_active by walking each surfaced session's
# compression-continuation chain forward in SQL and taking the MAX
@@ -1638,6 +1649,28 @@ class SessionDB:
# compression-continuation edges using the same criteria as
# get_compression_tip (parent.end_reason='compression' AND
# child.started_at >= parent.ended_at).
outer_where = where_sql
id_params: List[Any] = []
if id_needle:
# Admit a surfaced row if its own id or any id in its forward
# compression chain matches the needle. LIKE with a leading
# wildcard can't use an index, but the chain membership and
# the small result set keep this bounded — far cheaper than
# fetching every session and scanning in Python.
id_clause = (
"EXISTS (SELECT 1 FROM chain cq"
" WHERE cq.root_id = s.id"
" AND LOWER(cq.cur_id) LIKE ? ESCAPE '\\')"
)
like_pattern = (
"%"
+ id_needle.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
+ "%"
)
id_params = [like_pattern]
outer_where = (
f"{where_sql} AND {id_clause}" if where_sql else f"WHERE {id_clause}"
)
query = f"""
WITH RECURSIVE chain(root_id, cur_id) AS (
SELECT s.id, s.id FROM sessions s {where_sql}
@@ -1674,12 +1707,13 @@ class SessionDB:
COALESCE(cm.effective_last_active, s.started_at) AS _effective_last_active
FROM sessions s
LEFT JOIN chain_max cm ON cm.root_id = s.id
{where_sql}
{outer_where}
ORDER BY _effective_last_active DESC, s.started_at DESC, s.id DESC
LIMIT ? OFFSET ?
"""
# WHERE params apply twice (CTE seed + outer select).
params = params + params + [limit, offset]
# WHERE params apply twice (CTE seed + outer select); the id filter
# only applies to the outer select.
params = params + params + id_params + [limit, offset]
else:
query = f"""
SELECT s.*,
@@ -3025,12 +3059,18 @@ class SessionDB:
if not needle or limit <= 0:
return []
scan_limit = max(limit, 10_000)
sessions = self.list_sessions_rich(
limit=scan_limit,
# SQL-bounded: list_sessions_rich pushes the id LIKE filter into the
# query (matching the row's own id AND any id in its forward
# compression chain), so we only materialize matching rows instead of
# scanning every session. Fetch a small multiple of `limit` so the
# in-Python exact/prefix/substring ranking below has enough candidates
# to order, then truncate.
candidates = self.list_sessions_rich(
limit=max(limit * 4, limit),
offset=0,
include_archived=include_archived,
order_by_last_active=True,
id_query=needle,
)
def score(row: Dict[str, Any]) -> int:
@@ -3042,14 +3082,11 @@ class SessionDB:
return 1
return 2
matches = [
(score(row), idx, row)
for idx, row in enumerate(sessions)
if needle in str(row.get("id") or "").lower()
or needle in str(row.get("_lineage_root_id") or "").lower()
]
matches.sort(key=lambda item: (item[0], item[1]))
return [row for _, _, row in matches[:limit]]
ranked = sorted(
enumerate(candidates),
key=lambda item: (score(item[1]), item[0]),
)
return [row for _, row in ranked[:limit]]
def search_sessions(
self,