From 4efec63a34eace86d6a5e299daeb5d3b4b5b159d Mon Sep 17 00:00:00 2001 From: helix4u <4317663+helix4u@users.noreply.github.com> Date: Thu, 25 Jun 2026 12:39:41 -0600 Subject: [PATCH] fix(tools): let session_search match session titles --- tests/tools/test_session_search.py | 42 ++++++++++++++ tools/session_search_tool.py | 91 ++++++++++++++++++++++++++++-- 2 files changed, 129 insertions(+), 4 deletions(-) diff --git a/tests/tools/test_session_search.py b/tests/tools/test_session_search.py index 4676375bd37..024070e6761 100644 --- a/tests/tools/test_session_search.py +++ b/tests/tools/test_session_search.py @@ -189,6 +189,48 @@ class TestDiscoveryShape: assert result["results"] == [] assert result["count"] == 0 + def test_query_can_match_session_title_without_message_hit(self, db): + db.create_session("s_fingerprint", source="cli") + db.set_session_title("s_fingerprint", "fingerprint-login") + db.append_message("s_fingerprint", role="user", content="Let's configure PAM for biometric auth") + db.append_message("s_fingerprint", role="assistant", content="Checking Linux auth settings.") + + result = json.loads(session_search(query="fingerprint-login", db=db)) + + assert result["success"] is True + assert result["count"] == 1 + hit = result["results"][0] + assert hit["session_id"] == "s_fingerprint" + assert hit["title"] == "fingerprint-login" + assert hit["matched_role"] == "session_title" + assert "Session title matched" in hit["snippet"] + + def test_title_query_strips_common_model_quoting(self, db): + db.create_session("s_fingerprint", source="cli") + db.set_session_title("s_fingerprint", "fingerprint-login") + db.append_message("s_fingerprint", role="user", content="PAM auth setup") + + result = json.loads(session_search(query="`fingerprint-login`", db=db)) + + assert result["success"] is True + assert result["results"][0]["session_id"] == "s_fingerprint" + assert result["results"][0]["matched_role"] == "session_title" + + def test_title_match_respects_current_session_filter(self, db): + db.create_session("s_current", source="cli") + db.set_session_title("s_current", "fingerprint-login") + db.append_message("s_current", role="user", content="PAM auth setup") + + result = json.loads(session_search( + query="fingerprint-login", + current_session_id="s_current", + db=db, + )) + + assert result["success"] is True + assert result["results"] == [] + assert result["count"] == 0 + def test_limit_clamped_to_max_10(self, db): _seed_modpack_sessions(db) # Pass huge limit; should not error and should cap diff --git a/tools/session_search_tool.py b/tools/session_search_tool.py index 05770619dc2..3a1040e5273 100644 --- a/tools/session_search_tool.py +++ b/tools/session_search_tool.py @@ -391,6 +391,78 @@ def _scroll( return json.dumps(response, ensure_ascii=False) +def _normalize_title_query(query: str) -> str: + """Strip common quoting the model may include around a remembered title.""" + return query.strip().strip("`'\"") + + +def _title_match_result( + db, + query: str, + current_lineage_root: Optional[str], +) -> Optional[Dict[str, Any]]: + """Return a discovery-shaped result when the query matches a session title.""" + title_query = _normalize_title_query(query) + if not title_query: + return None + + try: + session_id = db.resolve_session_by_title(title_query) + except Exception: + logging.debug("resolve_session_by_title failed for %r", title_query, exc_info=True) + return None + if not session_id: + return None + + lineage_root = _resolve_to_parent(db, session_id) + if current_lineage_root and lineage_root == current_lineage_root: + return None + + try: + session_meta = db.get_session(lineage_root) or db.get_session(session_id) or {} + except Exception: + logging.debug("get_session failed for title match %s", session_id, exc_info=True) + session_meta = {} + if session_meta.get("source") in _HIDDEN_SESSION_SOURCES: + return None + + try: + messages = db.get_messages(session_id) + except Exception: + logging.debug("get_messages failed for title match %s", session_id, exc_info=True) + messages = [] + + anchor_id = messages[0].get("id") if messages else None + if anchor_id is not None: + try: + view = db.get_anchored_view(session_id, anchor_id, window=5, bookend=3) + except Exception: + logging.debug("get_anchored_view failed for title match %s/%s", session_id, anchor_id, exc_info=True) + view = {} + else: + view = {} + + entry = { + "session_id": session_id, + "when": _format_timestamp(session_meta.get("started_at")), + "source": session_meta.get("source", "unknown"), + "model": session_meta.get("model") or "unknown", + "title": session_meta.get("title") or title_query, + "matched_role": "session_title", + "match_message_id": anchor_id, + "snippet": f"Session title matched: {session_meta.get('title') or title_query}", + "bookend_start": [_shape_message(m) for m in (view.get("bookend_start") or messages[:3])], + "messages": [_shape_message(m, anchor_id=anchor_id) for m in (view.get("window") or messages[:5])], + "bookend_end": [_shape_message(m) for m in (view.get("bookend_end") or messages[-3:])], + "messages_before": view.get("messages_before", 0), + "messages_after": view.get("messages_after", max(len(messages) - 5, 0)), + "_lineage_root": lineage_root, + } + if lineage_root and lineage_root != session_id: + entry["parent_session_id"] = lineage_root + return entry + + def _discover( db, query: str, @@ -401,6 +473,8 @@ def _discover( ) -> str: """Discovery shape: FTS5 + anchored window + bookends per hit. Single call.""" role_list = role_filter if role_filter else ["user", "assistant"] + current_lineage_root = _resolve_to_parent(db, current_session_id) if current_session_id else None + title_result = _title_match_result(db, query, current_lineage_root) try: raw_results = db.search_messages( @@ -415,7 +489,7 @@ def _discover( logging.error("FTS5 search failed: %s", e, exc_info=True) return tool_error(f"Search failed: {e}", success=False) - if not raw_results: + if not raw_results and not title_result: return json.dumps({ "success": True, "mode": "discover", @@ -425,13 +499,21 @@ def _discover( "message": "No matching sessions found.", }, ensure_ascii=False) - current_lineage_root = _resolve_to_parent(db, current_session_id) if current_session_id else None - # Dedupe by lineage. Keep the raw owning session_id on the surviving # row — only that pairs validly with the FTS5 match id for the anchored # window. parent_session_id is exposed separately when different. seen_sessions = {} + results = [] + + if title_result: + title_lineage = title_result.pop("_lineage_root", None) + if title_lineage: + seen_sessions[title_lineage] = {"_title_only": True} + results.append(title_result) + for r in raw_results: + if len(seen_sessions) >= limit: + break raw_sid = r["session_id"] resolved_sid = _resolve_to_parent(db, raw_sid) # Skip the current session lineage @@ -446,8 +528,9 @@ def _discover( if len(seen_sessions) >= limit: break - results = [] for lineage_root, match_info in seen_sessions.items(): + if match_info.get("_title_only"): + continue hit_sid = match_info.get("session_id") or lineage_root msg_id = match_info.get("id") try: