From cb71296ebda6e842e22fef458332cbfb507717b4 Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 7 Oct 2026 12:46:32 +0530 Subject: [PATCH] fix(photon): a threaded reply to our bubble stays "own" after a restart Cloud spectrum targets carry no direction and _sent_message_ids is in-memory, so after a gateway restart a swipe-reply to one of our bubbles hydrated the quoted text from the sent-text index but was marked as not ours: the agent saw [Replying to: "..."] instead of [Replying to your previous message: "..."]. Only our sends are indexed, so an index hit is itself proof the target was ours. --- plugins/platforms/photon/adapter.py | 18 ++++++++++-------- .../platforms/photon/test_threaded_replies.py | 18 +++++++++++++++++- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/plugins/platforms/photon/adapter.py b/plugins/platforms/photon/adapter.py index 0976983c75..1e0e8b89e9 100644 --- a/plugins/platforms/photon/adapter.py +++ b/plugins/platforms/photon/adapter.py @@ -835,14 +835,16 @@ class PhotonAdapter(BasePlatformAdapter): if content.get("type") == "reply" and isinstance(content.get("content"), dict) \ and content["content"].get("type") not in (None, "unknown"): target_id = content.get("targetMessageId") - reply_ctx = {"reply_to_message_id": target_id, - "reply_to_text": content.get("targetText") or None, - "reply_to_is_own_message": content.get("targetDirection") == "outbound" or bool( - target_id and target_id in self._sent_message_ids)} - if target_id and not reply_ctx["reply_to_text"]: - # spectrum often can't hydrate the target's text (e.g. our own sends, older - # bubbles): fall back to the local index of what we sent. - reply_ctx["reply_to_text"] = _lookup_sent_text(space_id, target_id) + target_text = content.get("targetText") or None + # spectrum often can't hydrate the target's text (e.g. our own sends, older + # bubbles): fall back to the local index of what we sent. + stored_text = _lookup_sent_text(space_id, target_id) if target_id and not target_text else None + # Cloud targets carry no direction and _sent_message_ids is in-memory, so after a + # restart an index hit is the only proof the target was ours (only our sends are indexed). + is_own = content.get("targetDirection") == "outbound" or bool( + target_id and target_id in self._sent_message_ids) or stored_text is not None + reply_ctx = {"reply_to_message_id": target_id, "reply_to_text": target_text or stored_text, + "reply_to_is_own_message": is_own} content = content["content"] ctype = content.get("type") diff --git a/tests/plugins/platforms/photon/test_threaded_replies.py b/tests/plugins/platforms/photon/test_threaded_replies.py index d1ebd1017a..c2ba5b67f4 100644 --- a/tests/plugins/platforms/photon/test_threaded_replies.py +++ b/tests/plugins/platforms/photon/test_threaded_replies.py @@ -57,7 +57,7 @@ def _capture_sidecar(adapter: PhotonAdapter, message_id: str = "out-1") -> List[ def _reply_event(inner: Dict[str, Any], *, message_id: str = "in-2", target_id: str = "out-0", - direction: str = "outbound", target_text: str | None = "earlier answer") -> Dict[str, Any]: + direction: str | None = "outbound", target_text: str | None = "earlier answer") -> Dict[str, Any]: return { "messageId": message_id, "space": {"id": DM, "type": "dm", "phone": PHONE}, @@ -143,6 +143,22 @@ async def test_reply_to_our_message_hydrates_quoted_text_from_sent_index(monkeyp assert handled[-1].reply_to_is_own_message is True +@pytest.mark.asyncio +async def test_reply_after_restart_is_still_marked_own(monkeypatch): + """Cloud targets carry no direction; after a restart the index hit alone proves the bubble was ours.""" + sender = _make_adapter(monkeypatch) + _capture_sidecar(sender, message_id="out-9") + await sender.send(DM, "the plan is A then B") + + restarted = _make_adapter(monkeypatch) # fresh in-memory _sent_message_ids + handled = _capture_handled(restarted, monkeypatch) + event = _reply_event({"type": "text", "text": "ok"}, target_id="out-9", direction=None, target_text=None) + await restarted._dispatch_inbound(event) + + assert handled[-1].reply_to_text == "the plan is A then B" + assert handled[-1].reply_to_is_own_message is True + + @pytest.mark.asyncio async def test_send_to_bare_phone_is_found_from_dm_guid(monkeypatch): """Cron/standalone sends often target the bare E.164; replies arrive in the DM GUID space."""