From 5cb21e3fb5231cf9e8857e7f0ea64144afe908d0 Mon Sep 17 00:00:00 2001 From: kenyonxu Date: Wed, 20 May 2026 13:32:40 +0800 Subject: [PATCH] fix(gateway): edit streamed message instead of sending duplicate when response_transformed When a transform_llm_output hook appends content after streaming, the previous fix skipped the final-send suppression which caused the full response to be sent as a NEW message (duplicate). Instead, edit the existing streamed message in-place to append the transformed content, then set already_sent=True. Added stream_consumer.message_id and .accumulated_text public properties. --- gateway/run.py | 22 ++++++++++++++++++++++ gateway/stream_consumer.py | 10 ++++++++++ 2 files changed, 32 insertions(+) diff --git a/gateway/run.py b/gateway/run.py index 82aea10cce7..33b9e6e4435 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -17692,6 +17692,28 @@ class GatewayRunner: _content_delivered, ) response["already_sent"] = True + elif not _is_empty_sentinel and _transformed and _sc is not None: + # Plugin hooks transformed the response after streaming — edit the + # existing streamed message instead of sending a duplicate. + _sc_msg_id = _sc.message_id + if _sc_msg_id: + try: + await _sc.adapter.edit_message( + chat_id=source.chat_id, + message_id=_sc_msg_id, + content=response["final_response"], + finalize=True, + ) + response["already_sent"] = True + logger.info( + "Edited streamed message %s for session %s to include plugin-transformed content.", + _sc_msg_id, session_key or "?", + ) + except Exception as _edit_err: + logger.warning( + "Failed to edit streamed message for session %s: %s", + session_key or "?", _edit_err, + ) # Schedule deletion of tracked temporary progress bubbles after the # final response lands. Failed runs skip this so bubbles remain as diff --git a/gateway/stream_consumer.py b/gateway/stream_consumer.py index 17214050919..8d74d3be8cf 100644 --- a/gateway/stream_consumer.py +++ b/gateway/stream_consumer.py @@ -192,6 +192,16 @@ class GatewayStreamConsumer: """True when the stream consumer delivered the final assistant reply.""" return self._final_response_sent + @property + def message_id(self) -> str | None: + """The Discord/chat message ID of the last-sent or edited message.""" + return self._message_id + + @property + def accumulated_text(self) -> str: + """The accumulated streamed text (without think-block content).""" + return self._accumulated + @property def final_content_delivered(self) -> bool: """True when the final response content reached the user, even if