Ver código fonte

Follow threads the send tool replies into, not only ones it opens

chatto_send_room called follow_thread only for a thread it created, so a reply
into an existing thread left the bot a participant it did not follow. Chatto
delivers a thread's replies to an account that follows it (or was mentioned in
it directly), so the conversation the agent just joined could go silent. The
adapter's own send() already follows every thread it posts into; the tool now
agrees. Follow stays best-effort and a plain room message follows nothing.
Paul Klumpp 2 semanas atrás
pai
commit
b2630a1a1e
3 arquivos alterados com 55 adições e 4 exclusões
  1. 1 1
      README.md
  2. 6 3
      adapter.py
  3. 48 0
      test_adapter.py

+ 1 - 1
README.md

@@ -364,7 +364,7 @@ Beyond answering inbound messages, the agent can act on Chatto on its own initia
 | Tool | What it does |
 |------|--------------|
 | `chatto_list_rooms` | Lists the rooms visible to the bot with their IDs, kinds and membership. Optional `name` filters case-insensitively by substring. Useful for turning a channel name into the `platform:chat_id` address a cron `deliver` target needs. |
-| `chatto_send_room` | Posts a message into a channel or DM room, by `room_id` or `room_name`. Can post inside an existing thread (`thread_root_event_id`) or open a new one (`create_thread`). |
+| `chatto_send_room` | Posts a message into a channel or DM room, by `room_id` or `room_name`. Can post inside an existing thread (`thread_root_event_id`) or open a new one (`create_thread`). Either way the bot follows the thread afterwards, so later replies in it reach the agent. |
 | `chatto_send_dm` | Posts a message into the DM room you already have with a person, addressed by `user_id` or `user_login`. |
 
 Three boundaries are deliberate, and each tool reports them as an error instead of half-doing the job:

+ 6 - 3
adapter.py

@@ -3977,9 +3977,12 @@ async def _send_room(args: dict) -> str:
             create_thread=create_thread,
         )
 
-        # A newly created thread is followed so later activity reaches the bot
-        # (mirrors the adapter's own thread handling).
-        if create_thread and root_event_id:
+        # Any thread the bot posts into is followed, not only one it opened:
+        # Chatto delivers a thread's replies to an account that follows it (or
+        # was mentioned in it directly), so replying into someone else's thread
+        # without following it can end the conversation the agent just joined.
+        # Mirrors the adapter's own send(), which follows every thread it uses.
+        if root_event_id:
             try:
                 await client.follow_thread(room.id, root_event_id)
             except Exception as exc:

+ 48 - 0
test_adapter.py

@@ -3723,6 +3723,54 @@ class TestPluginToolHandlers:
         assert result["success"] is True
         assert client.post_message.await_args.kwargs["thread_root_event_id"] == "msg-9"
 
+    async def test_send_room_follows_a_thread_it_replied_into(self):
+        """Replying into someone else's thread makes the bot a participant.
+        Chatto only delivers a thread's replies to an account that follows it
+        (or was mentioned in it directly), so an unfollowed reply can end the
+        conversation the agent just joined — the adapter's own send() follows
+        every thread it posts into, and the tool has to agree."""
+        room = _make_room("room-1", "Ops", RoomKind.CHANNEL)
+        client = self._stub_client()
+        client.get_room = AsyncMock(return_value=_make_room_state(room))
+        client.get_message = AsyncMock(
+            return_value=_make_message(message_id="msg-9", room_id="room-1")
+        )
+        result, _, _ = await self._run(
+            _handle_chatto_send_room,
+            {"room_id": "room-1", "message": "hi", "thread_root_event_id": "msg-9"},
+            client=client,
+            optional_mention_rooms="room-1",
+        )
+        assert result["success"] is True
+        client.follow_thread.assert_awaited_once_with("room-1", "msg-9")
+
+    async def test_send_room_follows_a_thread_it_created(self):
+        room = _make_room("room-1", "Ops", RoomKind.CHANNEL)
+        client = self._stub_client()
+        client.get_room = AsyncMock(return_value=_make_room_state(room))
+        result, _, _ = await self._run(
+            _handle_chatto_send_room,
+            {"room_id": "room-1", "message": "hi", "create_thread": True},
+            client=client,
+            optional_mention_rooms="room-1",
+        )
+        assert result["success"] is True
+        client.follow_thread.assert_awaited_once_with("room-1", "msg-1")
+
+    async def test_send_room_does_not_follow_a_plain_room_message(self):
+        """Nothing to follow: a message that opens no thread and joins none."""
+        room = _make_room("room-1", "Ops", RoomKind.CHANNEL)
+        client = self._stub_client()
+        client.get_room = AsyncMock(return_value=_make_room_state(room))
+        result, _, _ = await self._run(
+            _handle_chatto_send_room,
+            {"room_id": "room-1", "message": "hi"},
+            client=client,
+            optional_mention_rooms="room-1",
+        )
+        assert result["success"] is True
+        client.follow_thread.assert_not_awaited()
+
     # -- chunked delivery --
 
     async def test_send_room_reports_a_partial_delivery(self):