From 9412f2dd84f7d7e41d359aee703cc6ffc44c71c2 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Fri, 17 Jul 2026 03:11:30 -0700 Subject: [PATCH] fix(discord): persist streamed final delivery Carry the original reply anchor through stream metadata so a successful final Discord edit marks the recovered source message complete. --- gateway/stream_consumer.py | 2 ++ plugins/platforms/discord/adapter.py | 11 ++++++++++- .../test_discord_missed_message_backfill.py | 19 +++++++++++++++++++ tests/gateway/test_stream_consumer.py | 16 ++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index fca8bf43847f..f1aaa393829c 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -240,6 +240,8 @@ class GatewayStreamConsumer: final-message delivery. """ meta = dict(self.metadata) if self.metadata else {} + if self._initial_reply_to_id: + meta["reply_to_message_id"] = self._initial_reply_to_id if expect_edits: meta["expect_edits"] = True if final: diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index 041b349b5732..6daf2c5e8588 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -2977,6 +2977,7 @@ class DiscordAdapter(BasePlatformAdapter): content: str, *, finalize: bool = False, + metadata: Optional[Dict[str, Any]] = None, ) -> SendResult: """Edit a previously sent Discord message. @@ -3056,7 +3057,15 @@ class DiscordAdapter(BasePlatformAdapter): self._last_overflow_preview[_preview_key] = truncated else: raise - return SendResult(success=True, message_id=message_id) + result = SendResult(success=True, message_id=message_id) + if finalize: + self._record_discord_response( + reply_to=(metadata or {}).get("reply_to_message_id"), + result=result, + content=content, + final=True, + ) + return result except Exception as e: # pragma: no cover - defensive logging logger.error("[%s] Failed to edit Discord message %s: %s", self.name, message_id, e, exc_info=True) return SendResult(success=False, error=str(e)) diff --git a/tests/gateway/test_discord_missed_message_backfill.py b/tests/gateway/test_discord_missed_message_backfill.py index ed7e85e33300..2d54cdeda5ba 100644 --- a/tests/gateway/test_discord_missed_message_backfill.py +++ b/tests/gateway/test_discord_missed_message_backfill.py @@ -735,6 +735,25 @@ async def test_send_uses_notify_metadata_as_final_delivery_signal(adapter): assert adapter._discord_message_is_persistently_complete("94") is True +@pytest.mark.asyncio +async def test_final_stream_edit_marks_original_request_complete(adapter): + channel = FakeChannel(channel_id=123) + message = SimpleNamespace(edit=AsyncMock()) + channel.fetch_message = AsyncMock(return_value=message) + adapter._client.get_channel = lambda _channel_id: channel + + result = await adapter.edit_message( + "123", + "9009", + "complete streamed response", + finalize=True, + metadata={"reply_to_message_id": "102"}, + ) + + assert result.success is True + assert adapter._discord_message_is_persistently_complete("102") is True + + def test_disabled_recovery_does_not_create_hot_path_ledger(adapter, monkeypatch): monkeypatch.setenv("DISCORD_MISSED_MESSAGE_BACKFILL", "false") message = make_message(message_id=90) diff --git a/tests/gateway/test_stream_consumer.py b/tests/gateway/test_stream_consumer.py index cd49d3d74782..b43bacf046a5 100644 --- a/tests/gateway/test_stream_consumer.py +++ b/tests/gateway/test_stream_consumer.py @@ -9,6 +9,22 @@ import pytest from gateway.stream_consumer import GatewayStreamConsumer, StreamConsumerConfig +def test_stream_send_metadata_carries_original_reply_anchor(): + consumer = GatewayStreamConsumer( + adapter=MagicMock(), + chat_id="123", + initial_reply_to_id="456", + ) + + assert consumer._metadata_for_send(final=False) == { + "reply_to_message_id": "456", + } + assert consumer._metadata_for_send(final=True) == { + "reply_to_message_id": "456", + "notify": True, + } + + # ── _clean_for_display unit tests ────────────────────────────────────────