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.json groups (with requireMention: 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 instead
Or 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
Summary
The Matrix plugin's
isDirectMessage()uses amemberCount === 2heuristic that incorrectly classifies 2-person group rooms (admin channels, monitoring rooms, etc.) as direct messages. This causes them to route to themainsession instead of getting their own room-specific session.This check is unnecessary — Matrix already distinguishes DMs from groups at the protocol level via
m.directaccount data and theis_directflag onm.room.memberevents. The existing code already uses both of these. The memberCount heuristic only adds false positives.Steps to Reproduce
openclaw.jsongroups (withrequireMention: false, or @mention the bot):Expected: Message routes to session
matrix:channel:!roomId:serverActual: Message routes to session
mainVerify via Gateway
You can confirm the misrouting by checking the active sessions:
Or in the gateway logs:
Root Cause
File:
extensions/matrix/src/matrix/monitor/direct.tsFunction:
isDirectMessageincreateDirectRoomTracker()The detection logic follows this path:
The
memberCount === 2check at lines 86-90 fires before the proper state checks and returns early:Why this is unnecessary: Matrix has protocol-level DM detection built in. When a client creates a DM, it sets
is_direct: trueon the invite event and updatesm.directaccount 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
Proposed Fix
Remove the
memberCount === 2early return. MoveresolveMemberCount()to after the DM checks — it then only runs for confirmed group rooms (diagnostic logging), avoiding an unnecessary API call for every DM:Edge Cases
client.dms.isDm()hasDirectFlag()client.dms.isDm()No DM detection is lost. The only change: 2-person groups without any DM flags are no longer misclassified.
Environment
@vector-im/matrix-bot-sdk