fix(buzz): reply in-thread instead of flat channel posts

Buzz has no native thread_id; channel threading is entirely --reply-to on
the triggering event. Interim commentary and progress bubbles only passed
the anchor via metadata.reply_to_message_id (or not at all), so most Kathy
posts landed as new top-level messages and cluttered channels.

- Honor metadata.reply_to_message_id in BuzzAdapter.send
- Pass reply_to on stream commentary sends
- Treat buzz like slack/mattermost for progress thread resolution
- Set _progress_reply_to to the trigger event for buzz
- Add unit tests for adapter metadata and progress routing
This commit is contained in:
NanakoIce
2026-08-14 10:34:26 -07:00
committed by Teknium
parent cbeb925f0a
commit 66fa6e41c4
5 changed files with 49 additions and 2 deletions
+6
View File
@@ -30069,6 +30069,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
and source.thread_id
and event_message_id
)
or (
# Buzz has no native thread_id; threading is always via reply-to
# the triggering event id (channel clutter otherwise).
str(getattr(source.platform, "value", source.platform) or "").lower() == "buzz"
and event_message_id
)
or _relay_prospective_thread_id
else None
)
+2 -1
View File
@@ -2712,11 +2712,12 @@ class GatewayStreamConsumer:
# draft(final=true) — that would seal the live stream with
# interim text and orphan the true final into a plain-send
# duplicate (live finding, 2026-08-16 canary).
_md = dict(self.metadata) if self.metadata else {}
_md = self._metadata_for_send(final=False) or {}
_md["_interim_send"] = True
result = await self.adapter.send(
chat_id=self.chat_id,
content=text,
reply_to=self._initial_reply_to_id,
metadata=_md,
)
# Note: do NOT set _already_sent = True here.
+6 -1
View File
@@ -795,8 +795,13 @@ class BuzzAdapter(BasePlatformAdapter):
if not content:
return SendResult(success=False, error="Empty message")
args = ["messages", "send", "--channel", str(chat_id), "--content", "-"]
# Prefer the stable thread anchor from metadata.thread_id (Slack-style),
# then metadata.reply_to_message_id (gateway stream consumer /
# progress sends), then the explicit reply_to argument. Without
# reply_to_message_id, interim commentary posts flat in the channel.
meta = metadata or {}
reply_target = self._resolve_reply_anchor(
(metadata or {}).get("thread_id") or reply_to
meta.get("thread_id") or meta.get("reply_to_message_id") or reply_to
)
if reply_target and self._reply_to_mode != "off":
args += ["--reply-to", str(reply_target)]
+24
View File
@@ -899,6 +899,29 @@ class TestBuzzAdapterSend:
args, _stdin = cli.calls[0]
assert args[args.index("--reply-to") + 1] == "buzz-event-123"
@pytest.mark.asyncio
async def test_send_uses_metadata_reply_to_message_id(self):
"""Gateway stream/progress pass reply anchors via metadata.
Without honoring reply_to_message_id, mid-turn commentary posts as
new top-level channel messages instead of thread replies.
"""
adapter = _make_adapter()
adapter._channel_state[CHANNEL] = {"chat_type": "group", "last_ts": 0, "seen": {}}
cli = _ScriptedCli()
cli.script("messages", "send", {"accepted": True, "event_id": "evt-reply", "message": ""})
adapter._run_cli = cli
result = await adapter.send(
CHANNEL,
"threaded reply",
metadata={"reply_to_message_id": "root-event-abc"},
)
assert result.success is True
args, _stdin = cli.calls[0]
assert "--reply-to" in args
assert args[args.index("--reply-to") + 1] == "root-event-abc"
@pytest.mark.asyncio
async def test_send_prefers_stable_thread_root_over_latest_reply(self):
adapter = _make_adapter()
@@ -917,6 +940,7 @@ class TestBuzzAdapterSend:
assert args[args.index("--reply-to") + 1] == "stable-root"
@pytest.mark.asyncio
async def test_send_image_local_file_uses_file_flag(self, tmp_path):
img = tmp_path / "shot.png"
+11
View File
@@ -1925,3 +1925,14 @@ class TestSlackReplyInThreadProgressRouting:
event_message_id="1700000000.000100",
reply_in_thread=False,
) is None
def test_buzz_uses_event_message_id_as_progress_thread(self):
"""Buzz has no native thread_id; progress must reply-to the trigger."""
from gateway.run import _resolve_progress_thread_id
assert _resolve_progress_thread_id(
"buzz",
source_thread_id=None,
event_message_id="evt-trigger-001",
reply_in_thread=True,
) == "evt-trigger-001"