fix(feishu): skip reaction transition on resuming stream end
Stream-end events are emitted at the end of every assistant turn. When the agent has more tool-call rounds queued, the runner sets `_resuming=True` on the metadata. Without a guard, every intermediate stream end removed the OnIt reaction (the first one wins, since `_reaction_ids.pop` empties the slot) and re-added `done_emoji`, producing a DONE reaction after every tool call instead of only at final completion. Wrap the OnIt removal and `done_emoji` add in a `not _resuming` guard so the OnIt indicator persists across tool-call rounds and DONE fires exactly once when the agent's final response lands. `_resuming` already flows through outbound metadata (`nanobot/agent/loop.py:747`) and survives `_coalesce_stream_deltas` because pure `_stream_end` messages without `_stream_delta` skip the merge branch. Tests: - test_no_removal_when_resuming - test_done_emoji_only_on_final_stream_end
This commit is contained in:
@@ -1361,7 +1361,12 @@ class FeishuChannel(BaseChannel):
|
||||
# --- stream end: final update or fallback ---
|
||||
if meta.get("_stream_end"):
|
||||
message_id = meta.get("message_id")
|
||||
if message_id:
|
||||
# Only finalize the OnIt -> DONE reaction transition on the truly
|
||||
# final stream end. _resuming=True means the agent will keep
|
||||
# working (more tool-call rounds), so leave the reaction state
|
||||
# in place — otherwise the OnIt indicator disappears prematurely
|
||||
# and the DONE reaction fires after every tool call.
|
||||
if message_id and not meta.get("_resuming"):
|
||||
reaction_id = self._reaction_ids.pop(message_id, None)
|
||||
if reaction_id:
|
||||
await self._remove_reaction(message_id, reaction_id)
|
||||
|
||||
@@ -255,3 +255,61 @@ class TestStreamEndReactionCleanup:
|
||||
)
|
||||
|
||||
ch._remove_reaction.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_removal_when_resuming(self):
|
||||
"""_resuming=True means more tool-call rounds follow; reaction must persist."""
|
||||
ch = _make_channel()
|
||||
ch.config.done_emoji = "DONE"
|
||||
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
|
||||
text="partial", card_id="card_1", sequence=3, last_edit=0.0,
|
||||
)
|
||||
ch._reaction_ids["om_001"] = "rx_42"
|
||||
ch._client.cardkit.v1.card_element.content.return_value = MagicMock(success=MagicMock(return_value=True))
|
||||
ch._client.cardkit.v1.card.settings.return_value = MagicMock(success=MagicMock(return_value=True))
|
||||
ch._remove_reaction = AsyncMock()
|
||||
ch._add_reaction = AsyncMock()
|
||||
|
||||
await ch.send_delta(
|
||||
"oc_chat1", "",
|
||||
metadata={"_stream_end": True, "_resuming": True, "message_id": "om_001"},
|
||||
)
|
||||
|
||||
ch._remove_reaction.assert_not_called()
|
||||
ch._add_reaction.assert_not_called()
|
||||
# OnIt reaction id is still tracked for the eventual final stream end
|
||||
assert ch._reaction_ids.get("om_001") == "rx_42"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_done_emoji_only_on_final_stream_end(self):
|
||||
"""Across resuming rounds, done_emoji is added only on the final round."""
|
||||
ch = _make_channel()
|
||||
ch.config.done_emoji = "DONE"
|
||||
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
|
||||
text="t", card_id="card_1", sequence=3, last_edit=0.0,
|
||||
)
|
||||
ch._reaction_ids["om_001"] = "rx_42"
|
||||
ch._client.cardkit.v1.card_element.content.return_value = MagicMock(success=MagicMock(return_value=True))
|
||||
ch._client.cardkit.v1.card.settings.return_value = MagicMock(success=MagicMock(return_value=True))
|
||||
ch._remove_reaction = AsyncMock()
|
||||
ch._add_reaction = AsyncMock()
|
||||
|
||||
# Intermediate stream end (more tool calls coming).
|
||||
await ch.send_delta(
|
||||
"oc_chat1", "",
|
||||
metadata={"_stream_end": True, "_resuming": True, "message_id": "om_001"},
|
||||
)
|
||||
ch._remove_reaction.assert_not_called()
|
||||
ch._add_reaction.assert_not_called()
|
||||
|
||||
# Re-prime the stream buffer for the final round (the previous _stream_end popped it).
|
||||
ch._stream_bufs["oc_chat1"] = _FeishuStreamBuf(
|
||||
text="t", card_id="card_1", sequence=5, last_edit=0.0,
|
||||
)
|
||||
# Final stream end (resuming=False): OnIt removed, done_emoji added.
|
||||
await ch.send_delta(
|
||||
"oc_chat1", "",
|
||||
metadata={"_stream_end": True, "_resuming": False, "message_id": "om_001"},
|
||||
)
|
||||
ch._remove_reaction.assert_called_once_with("om_001", "rx_42")
|
||||
ch._add_reaction.assert_called_once_with("om_001", "DONE")
|
||||
|
||||
Reference in New Issue
Block a user