From 735a24384979c007f8642bd2928f2ffda6680443 Mon Sep 17 00:00:00 2001 From: wangjunwei Date: Tue, 30 Jun 2026 14:04:37 +0800 Subject: [PATCH] fix(weixin): keep stream buffer until send succeeds so retries can re-deliver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit send_delta popped the buffer before self.send ran, so a transient WeChat send failure dropped the completed streamed reply: ChannelManager _send_with_retry re-invokes the same _stream_end message, but the buffer was already gone, so the retry sent empty content and returned — turning a delivery retry into silent message loss. Build `full` from the buffer without popping, send, then clear only after a successful send. The _stream_end message's own content (set when the manager coalesces deltas into the end message) is folded into `full` via addition rather than appended to the buffer, so a retry recomputes the same `full` from an unchanged buffer instead of double-counting it. Co-Authored-By: Claude Opus 4.8 --- nanobot/channels/weixin.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/nanobot/channels/weixin.py b/nanobot/channels/weixin.py index 75e6ddcc..fdb52d10 100644 --- a/nanobot/channels/weixin.py +++ b/nanobot/channels/weixin.py @@ -1246,16 +1246,26 @@ class WeixinChannel(BaseChannel): meta = metadata or {} if meta.get("_reasoning_delta") or meta.get("_reasoning"): return - if delta: + is_end = meta.get("_stream_end") + # Accumulate intermediate deltas. The _stream_end message's own content + # (present when the manager coalesces deltas into the end message) is + # folded into `full` below instead of appended here, so a send retry + # recomputes the same `full` from an unchanged buffer rather than + # double-counting that delta. + if delta and not is_end: self._stream_buffers.setdefault(chat_id, []).append(delta) - if not meta.get("_stream_end"): + if not is_end: return - full = "".join(self._stream_buffers.pop(chat_id, [])).strip() + full = ("".join(self._stream_buffers.get(chat_id, [])) + (delta or "")).strip() await self._flush_tool_hints(chat_id) if full: + # Send before clearing the buffer: if the send raises, the buffer is + # left intact so ChannelManager._send_with_retry can re-deliver the + # same _stream_end message instead of silently losing the reply. await self.send( OutboundMessage(channel=self.name, chat_id=chat_id, content=full) ) + self._stream_buffers.pop(chat_id, None) async def _start_typing(self, chat_id: str, context_token: str = "") -> None: """Start typing indicator immediately when a message is received."""