This commit is contained in:
Kongxi 2026-04-24 17:27:57 -05:00 committed by GitHub
commit 55ea728851
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 91 additions and 13 deletions

View file

@ -320,8 +320,8 @@ class TestSessionSearch:
assert result["sessions_searched"] == 1
assert current_sid not in [r.get("session_id") for r in result.get("results", [])]
def test_current_child_session_excludes_parent_lineage(self):
"""Compression/delegation parents should be excluded for the active child session."""
def test_current_child_session_excludes_delegation_parent(self):
"""Delegation parents (non-compression) should be excluded for the active child session."""
from unittest.mock import MagicMock
from tools.session_search_tool import session_search
@ -335,6 +335,7 @@ class TestSessionSearch:
if session_id == "child_sid":
return {"parent_session_id": "parent_sid"}
if session_id == "parent_sid":
# No end_reason — this is a delegation parent, not compression
return {"parent_session_id": None}
return None
@ -349,6 +350,71 @@ class TestSessionSearch:
assert result["results"] == []
assert result["sessions_searched"] == 0
def test_compression_parent_searchable(self):
"""Compression-ended parents should NOT be excluded — their content is
gone from the agent's context window after compaction."""
from unittest.mock import MagicMock
from tools.session_search_tool import session_search
mock_db = MagicMock()
mock_db.search_messages.return_value = [
{"session_id": "parent_sid", "content": "match", "source": "cli",
"session_started": 1709500000, "model": "test"},
]
def _get_session(session_id):
if session_id == "child_sid":
return {"parent_session_id": "parent_sid"}
if session_id == "parent_sid":
return {"parent_session_id": None, "end_reason": "compression"}
return None
mock_db.get_session.side_effect = _get_session
mock_db.get_messages_as_conversation.return_value = [
{"role": "user", "content": "test message"},
]
result = json.loads(session_search(
query="test", db=mock_db, current_session_id="child_sid",
))
assert result["success"] is True
# Compression parent should appear in results, not be excluded
assert result["sessions_searched"] >= 1
def test_multi_level_compression_parent_searchable(self):
"""Multi-level compaction: A→B→C, both A and B are compression parents.
When searching from C, both A and B should be searchable."""
from unittest.mock import MagicMock
from tools.session_search_tool import session_search
mock_db = MagicMock()
mock_db.search_messages.return_value = [
{"session_id": "session_a", "content": "match", "source": "cli",
"session_started": 1709500000, "model": "test"},
]
def _get_session(session_id):
if session_id == "session_c":
return {"parent_session_id": "session_b"}
if session_id == "session_b":
return {"parent_session_id": "session_a", "end_reason": "compression"}
if session_id == "session_a":
return {"parent_session_id": None, "end_reason": "compression"}
return None
mock_db.get_session.side_effect = _get_session
mock_db.get_messages_as_conversation.return_value = [
{"role": "user", "content": "test message"},
]
result = json.loads(session_search(
query="test", db=mock_db, current_session_id="session_c",
))
assert result["success"] is True
assert result["sessions_searched"] >= 1
def test_limit_none_coerced_to_default(self):
"""Model sends limit=null → should fall back to 3, not TypeError."""
from unittest.mock import MagicMock

View file

@ -374,16 +374,26 @@ def session_search(
# Resolve child sessions to their parent — delegation stores detailed
# content in child sessions, but the user's conversation is the parent.
def _resolve_to_parent(session_id: str) -> str:
"""Walk delegation chain to find the root parent session ID."""
def _resolve_to_parent(session_id: str) -> tuple:
"""Walk delegation chain to find the root parent session ID.
Returns (root_session_id, has_compression_hop) the second flag
is True when at least one session in the chain was ended by context
compression. Compression parents' content is no longer in the
agent's context window, so they should be searchable even if they
belong to the current lineage.
"""
visited = set()
sid = session_id
has_compression = False
while sid and sid not in visited:
visited.add(sid)
try:
session = db.get_session(sid)
if not session:
break
if session.get("end_reason") == "compression":
has_compression = True
parent = session.get("parent_session_id")
if parent:
sid = parent
@ -397,22 +407,24 @@ def session_search(
exc_info=True,
)
break
return sid
return sid, has_compression
current_lineage_root = (
_resolve_to_parent(current_session_id) if current_session_id else None
_current_root, _ = (
_resolve_to_parent(current_session_id) if current_session_id else (None, False)
)
# Group by resolved (parent) session_id, dedup, skip the current
# session lineage. Compression and delegation create child sessions
# that still belong to the same active conversation.
# session lineage — but NOT compression parents whose content is no
# longer in the agent's context window.
seen_sessions = {}
for result in raw_results:
raw_sid = result["session_id"]
resolved_sid = _resolve_to_parent(raw_sid)
# Skip the current session lineage — the agent already has that
# context, even if older turns live in parent fragments.
if current_lineage_root and resolved_sid == current_lineage_root:
resolved_sid, has_compression = _resolve_to_parent(raw_sid)
# Skip the current session lineage only when the content is still
# in the agent's context. Compression-ended sessions have been
# replaced by a compact summary — their original content is only
# reachable via search.
if _current_root and resolved_sid == _current_root and not has_compression:
continue
if current_session_id and raw_sid == current_session_id:
continue