fix(weixin): keep stream buffer until send succeeds so retries can re-deliver

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 <noreply@anthropic.com>
This commit is contained in:
wangjunwei
2026-06-30 22:43:03 +08:00
committed by Xubin Ren
co-authored by Claude Opus 4.8
parent edada598c8
commit 735a243849
+13 -3
View File
@@ -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."""