Skip to content

[Matrix] 2-person group rooms misrouted as DMs due to memberCount heuristic #19739

@derbronko

Description

@derbronko

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

  1. Configure OpenClaw with the Matrix plugin
  2. 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}'
  3. Invite the OpenClaw bot to the room, or have it auto-join
  4. Configure the room in openclaw.json groups (with requireMention: false, or @mention the bot):
    "groups": {
      "!roomId:server": { "requireMention": false, "allow": true }
    }
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions