-
-
Notifications
You must be signed in to change notification settings - Fork 69.6k
[Matrix] 2-person group rooms misrouted as DMs due to memberCount heuristic #19739
Description
Summary
The Matrix plugin's isDirectMessage() uses a memberCount === 2 heuristic that incorrectly classifies 2-person group rooms (admin channels, monitoring rooms, etc.) as direct messages. This causes them to route to the main session instead of getting their own room-specific session.
This check is unnecessary — Matrix already distinguishes DMs from groups at the protocol level via m.direct account data and the is_direct flag on m.room.member events. The existing code already uses both of these. The memberCount heuristic only adds false positives.
Steps to Reproduce
- Configure OpenClaw with the Matrix plugin
- Create a 2-person group room (not a DM):
curl -X POST "http://homeserver/_matrix/client/v3/createRoom" \ -H "Authorization: Bearer $TOKEN" \ -d '{"name": "Admin Channel", "invite": ["@user2:server"], "is_direct": false}'
- Invite the OpenClaw bot to the room, or have it auto-join
- Configure the room in
openclaw.jsongroups (withrequireMention: false, or @mention the bot):"groups": { "!roomId:server": { "requireMention": false, "allow": true } }
- Send a message in the room
Expected: Message routes to session matrix:channel:!roomId:server
Actual: Message routes to session main
Verify via Gateway
You can confirm the misrouting by checking the active sessions:
# In the OpenClaw gateway or session list, you'll see:
# ❌ No session for matrix:channel:!roomId:server
# ❌ Message appears in main session context insteadOr in the gateway logs:
matrix: dm detected via member count room=!roomId:server members=2 ← false positive
Root Cause
File: extensions/matrix/src/matrix/monitor/direct.ts
Function: isDirectMessage in createDirectRoomTracker()
The detection logic follows this path:
isDirectMessage(params)
│
├─ client.dms.isDm(roomId) ✅ Proper — checks m.direct account data (Matrix spec)
│ └─ if true → return true
│
├─ memberCount === 2 ❌ Heuristic — false positives on 2-person groups
│ └─ if true → return true (short-circuits before state check)
│
├─ hasDirectFlag(senderId) ✅ Proper — checks is_direct on m.room.member state
├─ hasDirectFlag(selfUserId) ✅ Proper — same check for bot's own membership
│ └─ if either true → return true
│
└─ return false (it's a group)
The memberCount === 2 check at lines 86-90 fires before the proper state checks and returns early:
const memberCount = await resolveMemberCount(roomId);
if (memberCount === 2) {
log(`matrix: dm detected via member count room=${roomId} members=${memberCount}`);
return true; // ← FALSE POSITIVE: any 2-person room treated as DM
}Why this is unnecessary: Matrix has protocol-level DM detection built in. When a client creates a DM, it sets is_direct: true on the invite event and updates m.direct account data. Both are already checked by the existing code. A room with 2 members but without these flags is explicitly a group room — the member count is not a signal.
Impact
- 2-person admin/monitoring channels lose session isolation
- Context leakage — conversations from different rooms bleed into one session
- Orphaned sessions — existing room-specific sessions get abandoned when routing changes
- Affects any Matrix deployment with 2-person group rooms
Proposed Fix
Remove the memberCount === 2 early return. Move resolveMemberCount() to after the DM checks — it then only runs for confirmed group rooms (diagnostic logging), avoiding an unnecessary API call for every DM:
--- a/extensions/matrix/src/matrix/monitor/direct.ts
+++ b/extensions/matrix/src/matrix/monitor/direct.ts
@@ -83,12 +83,6 @@
return true;
}
- const memberCount = await resolveMemberCount(roomId);
- if (memberCount === 2) {
- log(`matrix: dm detected via member count room=${roomId} members=${memberCount}`);
- return true;
- }
-
const selfUserId = params.selfUserId ?? (await ensureSelfUserId());
const directViaState =
(await hasDirectFlag(roomId, senderId)) || (await hasDirectFlag(roomId, selfUserId ?? ""));
@@ -97,6 +91,7 @@
return true;
}
+ const memberCount = await resolveMemberCount(roomId);
log(`matrix: dm check room=${roomId} result=group members=${memberCount ?? "unknown"}`);
return false;Edge Cases
| Scenario | m.direct | is_direct | Members | Caught by | Still works? |
|---|---|---|---|---|---|
| Normal DM | ✅ | ✅ | 2 | client.dms.isDm() |
✅ |
| DM without m.direct (e.g. legacy) | ❌ | ✅ | 2 | hasDirectFlag() |
✅ |
| DM without is_direct (e.g. broken client) | ✅ | ❌ | 2 | client.dms.isDm() |
✅ |
| 2-person group | ❌ | ❌ | 2 | None (correct!) | ✅ fixed |
| 3+ person group | ❌ | ❌ | 3+ | None (correct) | ✅ |
No DM detection is lost. The only change: 2-person groups without any DM flags are no longer misclassified.
Environment
- OpenClaw 2026.2.15
- Matrix plugin with
@vector-im/matrix-bot-sdk - Tested with Dendrite and Synapse homeservers