diff --git a/plugins/memory/honcho/session.py b/plugins/memory/honcho/session.py index 5436f24fd..d8c6c0e63 100644 --- a/plugins/memory/honcho/session.py +++ b/plugins/memory/honcho/session.py @@ -1087,7 +1087,17 @@ class HonchoSessionManager: try: observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer) - return self._fetch_peer_card(observer_peer_id, target=target_peer_id) + card = self._fetch_peer_card(observer_peer_id, target=target_peer_id) + if card: + return card + # Honcho self-hosted v3 stores the peer card on the peer itself + # (GET /peers/{id}/card). The observer-target slot used above is + # only populated when writes also go through that path. Fall back + # to the target peer's own card so honcho_profile works regardless + # of which write path populated it. + if target_peer_id: + return self._fetch_peer_card(target_peer_id) + return [] except Exception as e: logger.debug("Failed to fetch peer card from Honcho: %s", e) return [] @@ -1234,13 +1244,22 @@ class HonchoSessionManager: if not session: return None try: - peer_id = self._resolve_peer_id(session, peer) - if peer_id is None: + observer_peer_id, target_peer_id = self._resolve_observer_target(session, peer) + if observer_peer_id is None: logger.warning("Could not resolve peer '%s' for set_peer_card in session '%s'", peer, session_key) return None - peer_obj = self._get_or_create_peer(peer_id) - result = peer_obj.set_card(card) - logger.info("Updated peer card for %s (%d facts)", peer_id, len(card)) + peer_obj = self._get_or_create_peer(observer_peer_id) + result = ( + peer_obj.set_card(card, target=target_peer_id) + if target_peer_id is not None + else peer_obj.set_card(card) + ) + logger.info( + "Updated peer card observer=%s target=%s (%d facts)", + observer_peer_id, + target_peer_id or observer_peer_id, + len(card), + ) return result except Exception as e: logger.error("Failed to set peer card: %s", e) diff --git a/tests/honcho_plugin/test_session.py b/tests/honcho_plugin/test_session.py index 40b1b8d85..cd9670af2 100644 --- a/tests/honcho_plugin/test_session.py +++ b/tests/honcho_plugin/test_session.py @@ -212,6 +212,39 @@ class TestPeerLookupHelpers: assert mgr.get_peer_card(session.key) == ["Name: Robert"] assistant_peer.get_card.assert_called_once_with(target=session.user_peer_id) + def test_get_peer_card_falls_back_to_target_peer_own_card(self): + # When the observer-target card slot is empty (returns None/[]), fall + # back to the target peer's own card. Self-hosted Honcho v3 stores the + # peer card on the peer itself; the observer-target slot is only + # populated when writes also go through that path. + mgr, session = self._make_cached_manager() + assistant_peer = MagicMock() + assistant_peer.get_card.return_value = None # observer-target slot empty + user_peer = MagicMock() + user_peer.get_card.return_value = ["Prefers: dark mode"] + + 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) + + assert mgr.get_peer_card(session.key) == ["Prefers: dark mode"] + assistant_peer.get_card.assert_called_once_with(target=session.user_peer_id) + user_peer.get_card.assert_called_once_with() + + def test_set_peer_card_uses_observer_target_in_ai_observe_others_mode(self): + # Writes must go to the same observer-target slot that reads check, + # so that a subsequent honcho_profile read returns what was written. + mgr, session = self._make_cached_manager() + assistant_peer = MagicMock() + assistant_peer.set_card.return_value = ["Role: user"] + mgr._get_or_create_peer = MagicMock(return_value=assistant_peer) + + result = mgr.set_peer_card(session.key, ["Role: user"]) + + assert result == ["Role: user"] + assistant_peer.set_card.assert_called_once_with(["Role: user"], target=session.user_peer_id) + def test_search_context_uses_assistant_perspective_with_target(self): mgr, session = self._make_cached_manager() assistant_peer = MagicMock()