mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix(session_search): allow compression-ended parents to appear in search results
Compression-ended parent sessions contain content no longer in the agent's context window (replaced by a compact summary). The previous lineage exclusion logic skipped them along with delegation parents, creating a memory black hole for compacted content. Changes: - _resolve_to_parent() now returns (root_id, has_compression_hop), reusing the db.get_session() call already made during parent traversal (no extra queries) - Lineage exclusion skips delegation parents but allows compression parents - Tests: delegation exclusion, single compression, multi-level compaction 37 tests pass. Closes #13840
This commit is contained in:
parent
ff9752410a
commit
d9f19d4022
2 changed files with 91 additions and 13 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue