openclaw - 💡(How to fix) Fix [matrix] heartbeat delivery uses wrong account's Matrix user when target is room ID [2 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#52152Fetched 2026-04-08 01:15:02
View on GitHub
Comments
2
Participants
2
Timeline
4
Reactions
0
Timeline (top)
commented ×2closed ×1locked ×1

Error Message

15:56:37 | {"subsystem":"gateway/heartbeat"} | {"accountId": "agent_b", "target": "matrix", "channel": "matrix"} | heartbeat: using explicit accountId 16:05:38 | MatrixHttpClient (REQ-124) | errcode: 'M_FORBIDDEN', error: 'User @agent_a:matrix.org not in room !ROOM_B:matrix.org' 16:05:38 | {"subsystem":"gateway/heartbeat"} | {"error": "M_FORBIDDEN: User @agent_a:matrix.org not in room !ROOM_B:matrix.org"} | heartbeat failed

Root Cause

The directRoomCache in extensions/matrix/src/matrix/send/targets.ts was keyed by user ID only (@target_user), without considering which Matrix account performed the lookup.

// BUGGY: single-level cache keyed by userId only
const directRoomCache = new Map<string, string>();  // key = "@target_user"
directRoomCache.get("@target_user");  // shared across all accounts → wrong room

Code Example

M_FORBIDDEN: User @agent_a:matrix.org not in room !ROOM_B:matrix.org

---

// BUGGY: single-level cache keyed by userId only
const directRoomCache = new Map<string, string>();  // key = "@target_user"
directRoomCache.get("@target_user");  // shared across all accounts → wrong room

---

15:56:37 | {"subsystem":"gateway/heartbeat"} | {"accountId": "agent_b", "target": "matrix", "channel": "matrix"} | heartbeat: using explicit accountId
16:05:38 | MatrixHttpClient (REQ-124) | errcode: 'M_FORBIDDEN', error: 'User @agent_a:matrix.org not in room !ROOM_B:matrix.org'
16:05:38 | {"subsystem":"gateway/heartbeat"} | {"error": "M_FORBIDDEN: User @agent_a:matrix.org not in room !ROOM_B:matrix.org"} | heartbeat failed
RAW_BUFFERClick to expand / collapse

Bug Description

When two Matrix accounts each have a different DM room for the same target user, and both heartbeats use to: @target_user, the second heartbeat retrieves the first account's cached DM room and attempts to send from the wrong Matrix user — resulting in M_FORBIDDEN.

Reproduction Setup

Two agents, each with their own Matrix account (agent_a and agent_b), both configured with:

  • target: matrix
  • to: @target_user (the same user, e.g. @patrick:matrix.org)

Each account has its own m.direct entry mapping @target_user to a different room:

  • Account A: @target_user → !ROOM_A
  • Account B: @target_user → !ROOM_B

Steps to Reproduce

  1. Configure two agents with separate Matrix accounts, each with target: matrix and the same user ID as to
  2. Ensure each account has a different m.direct entry for that user
  3. Trigger Agent A's heartbeat first → finds !ROOM_A via m.directcaches {@target_user → !ROOM_A}
  4. Trigger Agent B's heartbeat → cache hit (keyed only by @target_user, not by account) → returns !ROOM_A → attempts to send to !ROOM_A from @agent_a:matrix.orgM_FORBIDDEN

Expected Behavior

Agent A → m.direct lookup → !ROOM_A → sent from @agent_a:matrix.org → succeeds
Agent B → m.direct lookup → !ROOM_B → sent from @agent_b:matrix.org → succeeds

Each account's DM room for the same target user should be isolated.

Actual Behavior

Agent A's heartbeat succeeds. Agent B's heartbeat fails with:

M_FORBIDDEN: User @agent_a:matrix.org not in room !ROOM_B:matrix.org

Agent B retrieves Agent A's cached room (!ROOM_A) because the cache key does not include account identity.

Root Cause

The directRoomCache in extensions/matrix/src/matrix/send/targets.ts was keyed by user ID only (@target_user), without considering which Matrix account performed the lookup.

// BUGGY: single-level cache keyed by userId only
const directRoomCache = new Map<string, string>();  // key = "@target_user"
directRoomCache.get("@target_user");  // shared across all accounts → wrong room

Fix (upstream resolution)

Upstream resolves this using WeakMap<MatrixClient, Map<string, string>> — keyed by client instance rather than by string-concatenated accountId. This is cleaner than string key concatenation because:

  1. No collision risk: string concatenation like "agent_b:@target_user" breaks if userId contains :. A WeakMap<MatrixClient, Map<...>> has no such issue.
  2. Natural account isolation: each MatrixClient instance already represents a specific Matrix account. Keying the cache by client instance is the most direct way to express "per-account cache".
  3. Automatic invalidation: WeakMap entries are garbage-collected when the client is no longer reachable, eliminating stale cache entries for disconnected accounts.
  4. Type safety: the two-level Map<accountId, Map<userId, roomId>> approach requires careful handling of nullable accountId (e.g. "default" fallback). The WeakMap approach sidesteps this entirely by using object identity.

Relevant Log Evidence

15:56:37 | {"subsystem":"gateway/heartbeat"} | {"accountId": "agent_b", "target": "matrix", "channel": "matrix"} | heartbeat: using explicit accountId
16:05:38 | MatrixHttpClient (REQ-124) | errcode: 'M_FORBIDDEN', error: 'User @agent_a:matrix.org not in room !ROOM_B:matrix.org'
16:05:38 | {"subsystem":"gateway/heartbeat"} | {"error": "M_FORBIDDEN: User @agent_a:matrix.org not in room !ROOM_B:matrix.org"} | heartbeat failed

Environment

  • OpenClaw: built from source, latest main
  • Node.js: 22.22.0
  • Matrix extension: extensions/matrix
  • Two Matrix accounts configured as separate agents

extent analysis

Fix Plan

To resolve the issue, we need to modify the directRoomCache to use a WeakMap keyed by the MatrixClient instance. This will ensure that each account's DM room for the same target user is isolated.

Here are the steps:

  • Replace the existing directRoomCache with a new WeakMap:
const directRoomCache = new WeakMap<MatrixClient, Map<string, string>>();
  • Update the get and set methods to use the MatrixClient instance as the key:
function getDirectRoom(client: MatrixClient, userId: string): string | undefined {
  const userCache = directRoomCache.get(client);
  if (!userCache) {
    return undefined;
  }
  return userCache.get(userId);
}

function setDirectRoom(client: MatrixClient, userId: string, roomId: string): void {
  let userCache = directRoomCache.get(client);
  if (!userCache) {
    userCache = new Map<string, string>();
    directRoomCache.set(client, userCache);
  }
  userCache.set(userId, roomId);
}
  • Use the updated getDirectRoom and setDirectRoom methods in the heartbeat logic.

Verification

To verify that the fix worked, you can test the following scenarios:

  • Trigger Agent A's heartbeat and verify that it succeeds.
  • Trigger Agent B's heartbeat and verify that it succeeds.
  • Check the logs to ensure that each agent is using the correct DM room for the target user.

Extra Tips

  • Make sure to update the directRoomCache to use the WeakMap approach to avoid any potential issues with string concatenation.
  • Consider adding additional logging to verify that the cache is being populated and used correctly.
  • If you encounter any issues with the WeakMap approach, you can try using a Map with a composite key (e.g. accountId + ":" + userId) as a fallback. However, this approach may have limitations and should be used with caution.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix [matrix] heartbeat delivery uses wrong account's Matrix user when target is room ID [2 comments, 2 participants]