diff --git a/nanobot/channels/matrix/runtime.py b/nanobot/channels/matrix/runtime.py index 70130bf57..f0d9187ef 100644 --- a/nanobot/channels/matrix/runtime.py +++ b/nanobot/channels/matrix/runtime.py @@ -968,6 +968,11 @@ class MatrixChannel(BaseChannel): meta["thread_reply_to_event_id"] = reply_to return meta + def _thread_session_key(self, room_id: str, event: RoomMessage) -> str | None: + if not (root_id := self._event_thread_root_id(event)): + return None + return f"{self.name}:{room_id}:thread:{root_id}" + @staticmethod def _build_thread_relates_to(metadata: dict[str, Any] | None) -> dict[str, Any] | None: if not metadata: @@ -1171,6 +1176,7 @@ class MatrixChannel(BaseChannel): await self._handle_message( sender_id=event.sender, chat_id=room.room_id, content=event.body, metadata=self._base_metadata(room, event), + session_key=self._thread_session_key(room.room_id, event), is_dm=self._is_direct_room(room), ) except Exception: @@ -1209,6 +1215,7 @@ class MatrixChannel(BaseChannel): content="\n".join(parts), media=[attachment["path"]] if attachment else [], metadata=meta, + session_key=self._thread_session_key(room.room_id, event), is_dm=self._is_direct_room(room), ) except Exception: diff --git a/nanobot/channels/matrix/tests/test_matrix_channel.py b/nanobot/channels/matrix/tests/test_matrix_channel.py index feef451a4..ae5729a52 100644 --- a/nanobot/channels/matrix/tests/test_matrix_channel.py +++ b/nanobot/channels/matrix/tests/test_matrix_channel.py @@ -971,6 +971,81 @@ async def test_on_message_sets_thread_metadata_when_threaded_event() -> None: assert metadata["thread_root_event_id"] == "$root1" assert metadata["thread_reply_to_event_id"] == "$reply1" assert metadata["event_id"] == "$reply1" + assert handled[0]["session_key"] == "matrix:!room:matrix.org:thread:$root1" + + +@pytest.mark.asyncio +async def test_on_message_keeps_matrix_thread_sessions_independent() -> None: + channel = MatrixChannel(_make_config(), MessageBus()) + client = _FakeAsyncClient("", "", "", None) + channel.client = client + + handled: list[dict[str, object]] = [] + + async def _fake_handle_message(**kwargs) -> None: + handled.append(kwargs) + + channel._handle_message = _fake_handle_message # type: ignore[method-assign] + + room = SimpleNamespace(room_id="!room:matrix.org", display_name="Test room", member_count=3) + + def _thread_event(body: str, event_id: str, root_id: str) -> SimpleNamespace: + return SimpleNamespace( + sender="@alice:matrix.org", + body=body, + event_id=event_id, + source={ + "content": { + "m.relates_to": { + "rel_type": "m.thread", + "event_id": root_id, + } + } + }, + ) + + await channel._on_message(room, _thread_event("Plan the wedding", "$reply1", "$root1")) + await channel._on_message(room, _thread_event("Pick a gift", "$reply2", "$root1")) + await channel._on_message(room, _thread_event("/new", "$reply3", "$root2")) + + assert [message["chat_id"] for message in handled] == [ + "!room:matrix.org", + "!room:matrix.org", + "!room:matrix.org", + ] + assert [message["session_key"] for message in handled] == [ + "matrix:!room:matrix.org:thread:$root1", + "matrix:!room:matrix.org:thread:$root1", + "matrix:!room:matrix.org:thread:$root2", + ] + assert handled[2]["content"] == "/new" + + +@pytest.mark.asyncio +async def test_on_message_keeps_non_threaded_room_session() -> None: + channel = MatrixChannel(_make_config(), MessageBus()) + client = _FakeAsyncClient("", "", "", None) + channel.client = client + + handled: list[dict[str, object]] = [] + + async def _fake_handle_message(**kwargs) -> None: + handled.append(kwargs) + + channel._handle_message = _fake_handle_message # type: ignore[method-assign] + + room = SimpleNamespace(room_id="!room:matrix.org", display_name="Test room", member_count=3) + event = SimpleNamespace( + sender="@alice:matrix.org", + body="Hello", + event_id="$event1", + source={"content": {}}, + ) + + await channel._on_message(room, event) + + assert len(handled) == 1 + assert handled[0]["session_key"] is None @pytest.mark.asyncio @@ -1076,6 +1151,7 @@ async def test_on_media_message_sets_thread_metadata_when_threaded_event( assert metadata["thread_root_event_id"] == "$root1" assert metadata["thread_reply_to_event_id"] == "$event1" assert metadata["event_id"] == "$event1" + assert handled[0]["session_key"] == "matrix:!room:matrix.org:thread:$root1" @pytest.mark.asyncio