From 34e120bcbb4d9b2b76122a7f93745007a8833032 Mon Sep 17 00:00:00 2001 From: heyyyimmax Date: Thu, 12 Mar 2026 16:21:49 +0100 Subject: [PATCH] fix(gateway): enforce chat_id isolation for all DM sessions --- gateway/session.py | 27 +++++++++++++++++---------- tests/gateway/test_session.py | 8 ++++---- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/gateway/session.py b/gateway/session.py index 2f74d454..1778c2e4 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -321,25 +321,32 @@ def build_session_key(source: SessionSource) -> str: This is the single source of truth for session key construction. DM rules: - - WhatsApp DMs include chat_id (multi-user support). - - Other DMs include thread_id when present (e.g. Slack threaded DMs), - so each DM thread gets its own session while top-level DMs share one. - - Without thread_id or chat_id, all DMs share a single session. + - DMs include chat_id when present, so each private conversation is isolated. + - thread_id further differentiates threaded DMs within the same DM chat. + - Without chat_id, thread_id is used as a best-effort fallback. + - Without thread_id or chat_id, DMs share a single session. Group/channel rules: - - thread_id differentiates threads within a channel. - - Without thread_id, all messages in a channel share one session. + - chat_id identifies the parent group/channel. + - thread_id differentiates threads within that parent chat. + - Without identifiers, messages fall back to one session per platform/chat_type. """ platform = source.platform.value if source.chat_type == "dm": + if source.chat_id: + if source.thread_id: + return f"agent:main:{platform}:dm:{source.chat_id}:{source.thread_id}" + return f"agent:main:{platform}:dm:{source.chat_id}" if source.thread_id: return f"agent:main:{platform}:dm:{source.thread_id}" - if platform == "whatsapp" and source.chat_id: - return f"agent:main:{platform}:dm:{source.chat_id}" return f"agent:main:{platform}:dm" + 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}" 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}" + return f"agent:main:{platform}:{source.chat_type}:{source.thread_id}" + return f"agent:main:{platform}:{source.chat_type}" class SessionStore: diff --git a/tests/gateway/test_session.py b/tests/gateway/test_session.py index 0737f18d..0f39ca6a 100644 --- a/tests/gateway/test_session.py +++ b/tests/gateway/test_session.py @@ -338,7 +338,7 @@ class TestSessionStoreRewriteTranscript: class TestWhatsAppDMSessionKeyConsistency: """Regression: all session-key construction must go through build_session_key - so WhatsApp DMs include chat_id while other DMs do not.""" + so DMs are isolated by chat_id across platforms.""" @pytest.fixture() def store(self, tmp_path): @@ -369,15 +369,15 @@ class TestWhatsAppDMSessionKeyConsistency: ) assert store._generate_session_key(source) == build_session_key(source) - def test_telegram_dm_omits_chat_id(self): - """Non-WhatsApp DMs should still omit chat_id (single owner DM).""" + def test_telegram_dm_includes_chat_id(self): + """Non-WhatsApp DMs should also include chat_id to separate users.""" source = SessionSource( platform=Platform.TELEGRAM, chat_id="99", chat_type="dm", ) key = build_session_key(source) - assert key == "agent:main:telegram:dm" + assert key == "agent:main:telegram:dm:99" def test_discord_group_includes_chat_id(self): """Group/channel keys include chat_type and chat_id."""