diff --git a/gateway/session.py b/gateway/session.py index 1778c2e43dc..3dafa1781f4 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -328,7 +328,9 @@ def build_session_key(source: SessionSource) -> str: Group/channel rules: - chat_id identifies the parent group/channel. + - user_id/user_id_alt isolates participants within that parent chat when available. - thread_id differentiates threads within that parent chat. + - Without participant identifiers, messages fall back to one shared session per chat. - Without identifiers, messages fall back to one session per platform/chat_type. """ platform = source.platform.value @@ -340,13 +342,18 @@ def build_session_key(source: SessionSource) -> str: if source.thread_id: return f"agent:main:{platform}:dm:{source.thread_id}" return f"agent:main:{platform}:dm" + + participant_id = source.user_id_alt or source.user_id + key_parts = ["agent:main", platform, source.chat_type] + if source.chat_id: - if source.thread_id: - return f"agent:main:{platform}:{source.chat_type}:{source.chat_id}:{source.thread_id}" - return f"agent:main:{platform}:{source.chat_type}:{source.chat_id}" + key_parts.append(source.chat_id) if source.thread_id: - return f"agent:main:{platform}:{source.chat_type}:{source.thread_id}" - return f"agent:main:{platform}:{source.chat_type}" + key_parts.append(source.thread_id) + if participant_id: + key_parts.append(str(participant_id)) + + return ":".join(key_parts) class SessionStore: diff --git a/tests/gateway/test_session.py b/tests/gateway/test_session.py index cd0104aceb6..e9e629fe6ed 100644 --- a/tests/gateway/test_session.py +++ b/tests/gateway/test_session.py @@ -369,6 +369,29 @@ class TestWhatsAppDMSessionKeyConsistency: ) assert store._generate_session_key(source) == build_session_key(source) + def test_store_creates_distinct_group_sessions_per_user(self, store): + first = SessionSource( + platform=Platform.DISCORD, + chat_id="guild-123", + chat_type="group", + user_id="alice", + user_name="Alice", + ) + second = SessionSource( + platform=Platform.DISCORD, + chat_id="guild-123", + chat_type="group", + user_id="bob", + user_name="Bob", + ) + + first_entry = store.get_or_create_session(first) + second_entry = store.get_or_create_session(second) + + assert first_entry.session_key == "agent:main:discord:group:guild-123:alice" + assert second_entry.session_key == "agent:main:discord:group:guild-123:bob" + assert first_entry.session_id != second_entry.session_id + def test_telegram_dm_includes_chat_id(self): """Non-WhatsApp DMs should also include chat_id to separate users.""" source = SessionSource( @@ -398,6 +421,24 @@ class TestWhatsAppDMSessionKeyConsistency: key = build_session_key(source) assert key == "agent:main:discord:group:guild-123" + def test_group_sessions_are_isolated_per_user_when_user_id_present(self): + first = SessionSource( + platform=Platform.DISCORD, + chat_id="guild-123", + chat_type="group", + user_id="alice", + ) + second = SessionSource( + platform=Platform.DISCORD, + chat_id="guild-123", + chat_type="group", + user_id="bob", + ) + + assert build_session_key(first) == "agent:main:discord:group:guild-123:alice" + assert build_session_key(second) == "agent:main:discord:group:guild-123:bob" + assert build_session_key(first) != build_session_key(second) + def test_group_thread_includes_thread_id(self): """Forum-style threads need a distinct session key within one group.""" source = SessionSource( @@ -409,6 +450,17 @@ class TestWhatsAppDMSessionKeyConsistency: key = build_session_key(source) assert key == "agent:main:telegram:group:-1002285219667:17585" + def test_group_thread_sessions_are_isolated_per_user(self): + source = SessionSource( + platform=Platform.TELEGRAM, + chat_id="-1002285219667", + chat_type="group", + thread_id="17585", + user_id="42", + ) + key = build_session_key(source) + assert key == "agent:main:telegram:group:-1002285219667:17585:42" + class TestSessionStoreEntriesAttribute: """Regression: /reset must access _entries, not _sessions."""