fix(session-search): report source from resolved parent, not FTS5 child session (#15909)

When a delegation child session (e.g. source='telegram') contains the
FTS5 hit but _resolve_to_parent() maps it to a different root session
(source='api_server'), the result entry was still reporting the child's
source because the loop discarded session_meta as `_` and fell back to
match_info.get('source'), which carries the child session's value.

Use the resolved parent's session_meta for source, model, and started_at
with match_info as a fallback, so the output accurately reflects the
session the user actually interacted with.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
briandevans 2026-04-25 23:24:56 -07:00 committed by Teknium
parent b46b0c9888
commit 6b4ccb9b14
2 changed files with 73 additions and 4 deletions

View file

@ -498,3 +498,65 @@ class TestSessionSearch:
assert result["count"] == 0 assert result["count"] == 0
assert result["results"] == [] assert result["results"] == []
assert result["sessions_searched"] == 0 assert result["sessions_searched"] == 0
def test_source_from_resolved_parent_not_fts5_child(self):
"""source in output must reflect the resolved parent session, not the child that matched FTS5.
Regression test for #15909: when a delegation child session (source='telegram')
resolves to a parent (source='api_server'), the result entry must report
'api_server', not 'telegram'.
"""
from unittest.mock import MagicMock, AsyncMock, patch as _patch
from tools.session_search_tool import session_search
mock_db = MagicMock()
# FTS5 hit is in the child delegation session which carries source='telegram'
mock_db.search_messages.return_value = [
{
"session_id": "child_sid",
"content": "hello world",
"source": "telegram", # child session source — wrong value to surface
"session_started": 1709400000,
"model": "gpt-4o-mini",
},
]
def _get_session(session_id):
if session_id == "child_sid":
return {
"id": "child_sid",
"parent_session_id": "parent_sid",
"source": "telegram",
"started_at": 1709400000,
"model": "gpt-4o-mini",
}
if session_id == "parent_sid":
return {
"id": "parent_sid",
"parent_session_id": None,
"source": "api_server", # correct parent source
"started_at": 1709300000,
"model": "gpt-4o-mini",
}
return None
mock_db.get_session.side_effect = _get_session
mock_db.get_messages_as_conversation.return_value = [
{"role": "user", "content": "hello world"},
{"role": "assistant", "content": "hi there"},
]
with _patch(
"tools.session_search_tool.async_call_llm",
new_callable=AsyncMock,
side_effect=RuntimeError("no provider"),
):
result = json.loads(session_search(query="hello world", db=mock_db))
assert result["success"] is True
assert result["count"] == 1
entry = result["results"][0]
assert entry["session_id"] == "parent_sid", "should report resolved parent session ID"
assert entry["source"] == "api_server", (
f"source should be parent's 'api_server', got {entry['source']!r}"
)

View file

@ -486,7 +486,7 @@ def session_search(
}, ensure_ascii=False) }, ensure_ascii=False)
summaries = [] summaries = []
for (session_id, match_info, conversation_text, _), result in zip(tasks, results): for (session_id, match_info, conversation_text, session_meta), result in zip(tasks, results):
if isinstance(result, Exception): if isinstance(result, Exception):
logging.warning( logging.warning(
"Failed to summarize session %s: %s", "Failed to summarize session %s: %s",
@ -494,11 +494,18 @@ def session_search(
) )
result = None result = None
# Prefer resolved parent session metadata over FTS5 match metadata.
# match_info carries source/model from the *child* session that contained
# the FTS5 hit; after _resolve_to_parent() the session_id points to the
# root, so session_meta has the authoritative platform/source for the
# session the user actually cares about (#15909).
entry = { entry = {
"session_id": session_id, "session_id": session_id,
"when": _format_timestamp(match_info.get("session_started")), "when": _format_timestamp(
"source": match_info.get("source", "unknown"), session_meta.get("started_at") or match_info.get("session_started")
"model": match_info.get("model"), ),
"source": session_meta.get("source") or match_info.get("source", "unknown"),
"model": session_meta.get("model") or match_info.get("model"),
} }
if result: if result: