Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions plugins/memory/honcho/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,12 @@ def search_context(
search_query=query,
target=target,
)
# In directional observation mode the observer->target slot
# (e.g. hermes-about-Jesse) is often empty while the data lives
# on the target peer's own self-representation. Mirror the
# get_peer_card fallback so semantic search still returns results.
if not ctx["representation"] and not ctx["card"] and target and target != observer_peer_id:
ctx = self._fetch_peer_context(target, search_query=query)
parts = []
if ctx["representation"]:
parts.append(ctx["representation"])
Expand Down
67 changes: 67 additions & 0 deletions tests/honcho_plugin/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,73 @@ def test_search_context_accepts_explicit_ai_peer_id(self):
search_query="assistant",
)

def test_search_context_falls_back_to_target_self_context_when_directional_empty(self):
# In directional mode the observer->target slot (assistant-about-user)
# is often empty because conclusions are auto-derived onto the target
# peer's own self-representation, not written through the directional
# path. Mirror the get_peer_card fallback: when the directional fetch
# yields no representation and no card, refetch the target peer's own
# self-context so honcho_search still returns results.
mgr, session = self._make_cached_manager()
assistant_peer = MagicMock()
assistant_peer.context.return_value = SimpleNamespace(
representation="", # directional observer->target slot empty
peer_card=[],
)
assistant_peer.representation.return_value = ""
assistant_peer.get_card.return_value = None # directional card slot empty
user_peer = MagicMock()
user_peer.context.return_value = SimpleNamespace(
representation="Robert runs neuralancer",
peer_card=["Location: Melbourne"],
)

def _peer(peer_id: str) -> MagicMock:
return assistant_peer if peer_id == session.assistant_peer_id else user_peer

mgr._get_or_create_peer = MagicMock(side_effect=_peer)

result = mgr.search_context(session.key, "neuralancer")

assert "Robert runs neuralancer" in result
assert "- Location: Melbourne" in result
# Directional fetch attempted first against the user target...
assistant_peer.context.assert_called_once_with(
target=session.user_peer_id,
search_query="neuralancer",
)
# ...then fell back to the target peer's own self-context.
user_peer.context.assert_called_once_with(search_query="neuralancer")

def test_search_context_does_not_fall_back_when_directional_slot_populated(self):
# Guard the happy path: when the observer->target directional fetch
# returns data, search_context must NOT make a second fetch against
# the target peer. Protects against regressing existing behavior and
# against a redundant network call.
mgr, session = self._make_cached_manager()
assistant_peer = MagicMock()
assistant_peer.context.return_value = SimpleNamespace(
representation="Robert runs neuralancer",
peer_card=["Location: Melbourne"],
)
user_peer = MagicMock()

def _peer(peer_id: str) -> MagicMock:
return assistant_peer if peer_id == session.assistant_peer_id else user_peer

mgr._get_or_create_peer = MagicMock(side_effect=_peer)

result = mgr.search_context(session.key, "neuralancer")

assert "Robert runs neuralancer" in result
assert "- Location: Melbourne" in result
# Directional fetch served the result; the target peer was never touched.
assistant_peer.context.assert_called_once_with(
target=session.user_peer_id,
search_query="neuralancer",
)
user_peer.context.assert_not_called()

def test_get_prefetch_context_fetches_user_and_ai_from_peer_api(self):
mgr, session = self._make_cached_manager()
user_peer = MagicMock()
Expand Down