Summary
extensions/matrix/src/matrix/direct-room.ts classifies any 2-member room (bot + 1 user) as a "strict direct room" using a pure member-count heuristic. This happens before per-room/per-group config is consulted, so:
- A room explicitly configured under
channels.matrix.groups[<roomId>] with requireMention: true is still treated as a DM at runtime.
- A room created with
is_direct: false and a name is still treated as a DM.
- DM mode bypasses
requireMention, so the bot responds to every message in those rooms regardless of mention status.
The CHANGELOG entry from #57124 ("honor explicit local is_direct: false for discovered DM candidates") was a partial fix — only for inherited/discovered DM candidates, not for the runtime isStrictDirectRoom path that drives mention/skill/session routing.
Affected code
// extensions/matrix/src/matrix/direct-room.ts
export function isStrictDirectMembership(params: {
selfUserId?: string | null;
remoteUserId?: string | null;
joinedMembers?: readonly string[] | null;
}): boolean {
// ...
return Boolean(
selfUserId &&
remoteUserId &&
joinedMembers.length === 2 && // ← pure heuristic
joinedMembers.includes(selfUserId) &&
joinedMembers.includes(remoteUserId),
);
}
inspectMatrixDirectRoomEvidence does read the is_direct membership flag via hasDirectMatrixMemberFlag, but the result (memberStateFlag) is never used to negate the strict flag — even when explicitly false. isStrictDirectRoom returns .strict directly without considering is_direct: false or local config.
Reproduction (verified on 2026.5.19)
-
Configure OpenClaw with:
"channels.matrix": {
"enabled": true,
"encryption": true,
"dm": {
"enabled": true,
"policy": "allowlist",
"allowFrom": ["@user:server"],
"sessionScope": "per-room"
},
"groupPolicy": "open",
"groups": {
"!roomId:server": {
"requireMention": true,
"skills": ["..."]
}
}
}
-
Create a Matrix room via the client API with:
{
"name": "parents",
"preset": "private_chat",
"is_direct": false,
"initial_state": [
{ "type": "m.room.encryption", "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" } }
],
"invite": ["@user:server"]
}
-
Invite the bot, bot joins → room has 2 members. The room has a name, is_direct: false on the membership events, and is explicitly in OpenClaw's groups[] map.
-
From @user:server, send a plain message (no @bot mention) → bot responds despite requireMention: true.
-
Add a third member (any user) to the same room → restart not required, eventually joinedMembers.length === 3 so isStrictDirectMembership returns false → requireMention: true is now honored: plain messages ignored, only @bot ... triggers a reply.
The workaround "add a third member" confirms the diagnosis: the 2-person count is the sole DM classifier.
Why this matters
The pattern "private 1:1 room with the bot that should still require mentions" is reasonable: a personal scratch room for an admin, a held-aside room before adding family/team members, an OPS room where the user wants explicit invocation. The current code makes this impossible without inviting a placeholder third user.
The deeper issue: groups[<roomId>] config has no effect for any 2-person room. The schema's rooms vs groups distinction is silently broken for the smallest rooms.
Proposed fix shape
In isStrictDirectRoom (or its callers), treat as NOT a strict direct room when any of the following hold:
- The bot's own
m.room.member event has is_direct: false (already readable via hasDirectMatrixMemberFlag — just needs to be honored as a veto, not an augmentation).
- The room's
room_id is explicitly listed in channels.matrix.groups[].
- (Optional, more conservative) The room has an
m.room.name set — DMs are conventionally nameless.
Suggested minimal patch in inspectMatrixDirectRoomEvidence:
const memberStateFlag = await hasDirectMatrixMemberFlag(params.client, params.roomId, selfUserId);
// Explicit local override: is_direct === false negates the strict-DM classification
if (memberStateFlag === false) {
return {
joinedMembers,
strict: false,
viaMemberState: false,
memberStateFlag,
};
}
Then a separate change in the channel runtime to check groups[roomId] presence before routing through DM logic.
Environment
- OpenClaw: 2026.5.19
- Matrix homeserver: Synapse (latest)
- Matrix plugin: bundled
@openclaw/matrix
- Node: as shipped in
ghcr.io/openclaw/openclaw:latest
Related
Happy to draft a PR if the maintainers concur on the fix shape — wanted to file the issue first to get the design discussion in the right place.
Summary
extensions/matrix/src/matrix/direct-room.tsclassifies any 2-member room (bot + 1 user) as a "strict direct room" using a pure member-count heuristic. This happens before per-room/per-group config is consulted, so:channels.matrix.groups[<roomId>]withrequireMention: trueis still treated as a DM at runtime.is_direct: falseand a name is still treated as a DM.requireMention, so the bot responds to every message in those rooms regardless of mention status.The CHANGELOG entry from #57124 ("honor explicit local
is_direct: falsefor discovered DM candidates") was a partial fix — only for inherited/discovered DM candidates, not for the runtimeisStrictDirectRoompath that drives mention/skill/session routing.Affected code
inspectMatrixDirectRoomEvidencedoes read theis_directmembership flag viahasDirectMatrixMemberFlag, but the result (memberStateFlag) is never used to negate thestrictflag — even when explicitlyfalse.isStrictDirectRoomreturns.strictdirectly without consideringis_direct: falseor local config.Reproduction (verified on 2026.5.19)
Configure OpenClaw with:
Create a Matrix room via the client API with:
{ "name": "parents", "preset": "private_chat", "is_direct": false, "initial_state": [ { "type": "m.room.encryption", "state_key": "", "content": { "algorithm": "m.megolm.v1.aes-sha2" } } ], "invite": ["@user:server"] }Invite the bot, bot joins → room has 2 members. The room has a name,
is_direct: falseon the membership events, and is explicitly in OpenClaw'sgroups[]map.From
@user:server, send a plain message (no@botmention) → bot responds despiterequireMention: true.Add a third member (any user) to the same room → restart not required, eventually
joinedMembers.length === 3soisStrictDirectMembershipreturnsfalse→requireMention: trueis now honored: plain messages ignored, only@bot ...triggers a reply.The workaround "add a third member" confirms the diagnosis: the 2-person count is the sole DM classifier.
Why this matters
The pattern "private 1:1 room with the bot that should still require mentions" is reasonable: a personal scratch room for an admin, a held-aside room before adding family/team members, an OPS room where the user wants explicit invocation. The current code makes this impossible without inviting a placeholder third user.
The deeper issue:
groups[<roomId>]config has no effect for any 2-person room. The schema'sroomsvsgroupsdistinction is silently broken for the smallest rooms.Proposed fix shape
In
isStrictDirectRoom(or its callers), treat as NOT a strict direct room when any of the following hold:m.room.memberevent hasis_direct: false(already readable viahasDirectMatrixMemberFlag— just needs to be honored as a veto, not an augmentation).room_idis explicitly listed inchannels.matrix.groups[].m.room.nameset — DMs are conventionally nameless.Suggested minimal patch in
inspectMatrixDirectRoomEvidence:Then a separate change in the channel runtime to check
groups[roomId]presence before routing through DM logic.Environment
@openclaw/matrixghcr.io/openclaw/openclaw:latestRelated
Happy to draft a PR if the maintainers concur on the fix shape — wanted to file the issue first to get the design discussion in the right place.