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.
This commit is contained in:
kshitijk4poor
2026-10-07 12:46:32 +05:30
committed by kshitij
parent aba1f72ef2
commit cb71296ebd
2 changed files with 27 additions and 9 deletions

View File

@@ -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")

View File

@@ -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."""