openclaw - ✅(Solved) Fix [Bug]: Matrix outbound sends use lowercased room ID, triggering 403 M_FORBIDDEN against rooms the bot is actually joined to [2 pull requests, 1 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#78206Fetched 2026-05-07 03:39:43
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
2
Timeline (top)
cross-referenced ×2closed ×1commented ×1

Matrix outbound sends regularly target a lowercased variant of the canonical mixed-case room ID, causing Synapse to return 403 M_FORBIDDEN: User not in room <lowercased-id> even though the bot is a member of the canonical mixed-case room.

Root Cause

Outbound payload to field gets a lowercased identity that matches the canonicalized session/group key, not the joined room. Synapse correctly returns 403 because no room with the lowercased ID exists in that bot account's membership. The mixed-case destination is preserved in deliveryContext.to and lastTo in the session entry but is discarded somewhere in the outbound path.

Fix Action

Fixed

PR fix notes

PR #78214: fix: preserve Matrix room id case in sessions

Description (problem / solution / changelog)

Fixes #78206.

Summary

  • Preserve Matrix provider-exact room ID casing when gateway session persistence validates a group ID against a lowercased session-key-derived group ID.
  • Require the Matrix request delivery target (to: room:<groupId>) to match the provider-exact group ID before accepting a case-only variant, keeping normal group trust exact.
  • Add regressions for preserving the mixed-case Matrix room ID and rejecting a case-only mismatch without a matching delivery target.

Tests

  • PATH="/tmp/openclaw-pnpm-shim:$PATH" pnpm exec oxfmt --check src/gateway/server-methods/agent.ts src/gateway/server-methods/agent.test.ts
  • git diff --check
  • PATH="/tmp/openclaw-pnpm-shim:$PATH" node scripts/check-changed.mjs (blocked in unrelated existing core typecheck diagnostics: missing local @openclaw/fs-safe/* packages plus pre-existing strictness errors outside this patch; earlier lanes passed)
  • Targeted gateway vitest attempted, but startup is blocked locally by missing @openclaw/fs-safe/config imported from src/infra/fs-safe-defaults.ts.

Changed files

  • src/gateway/server-methods/agent.test.ts (modified, +23/-0)
  • src/gateway/server-methods/agent.ts (modified, +63/-4)

PR #78218: fix: preserve Matrix room id casing

Description (problem / solution / changelog)

Fixes #78206.

Summary

  • preserve provider-exact Matrix room IDs when session-derived group ids differ only by case
  • allow Matrix-only case variants through group trust so exact room IDs are not dropped later
  • keep non-Matrix case-only variants rejected

Tests

  • oxfmt --check on changed files
  • git diff --check
  • targeted gateway/agents vitest attempted but blocked before tests by missing local @openclaw/fs-safe/config
  • pnpm check:changed passed early lanes, failed on unrelated existing core typecheck issues including missing @openclaw/fs-safe/*

Changed files

  • src/agents/pi-tools.policy.test.ts (modified, +18/-0)
  • src/agents/pi-tools.policy.ts (modified, +12/-0)
  • src/gateway/server-methods/agent.test.ts (modified, +23/-0)
  • src/gateway/server-methods/agent.ts (modified, +63/-4)

Code Example

[tools] message failed: User @bot-a:matrix.example.org not in room
     !examplemixedcase:matrix.example.org, and room previews are disabled

---

Class                                  Count
case-bug (lowercased mixed-case room)  357
out-of-scope (other not-in-room)        53

---

2026-05-05T13:14:31Z [tools] message failed:
  User @bot-a:matrix.example.org not in room !examplemixedcase:matrix.example.org,
  and room previews are disabled
  raw_params={"action":"read","channel":"matrix",
             "target":"room:!examplemixedcase:matrix.example.org","limit":12}

---

POST /_matrix/client/v3/rooms/<roomId>/send/m.room.message/<txnId>
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Matrix outbound sends regularly target a lowercased variant of the canonical mixed-case room ID, causing Synapse to return 403 M_FORBIDDEN: User not in room <lowercased-id> even though the bot is a member of the canonical mixed-case room.

Steps to reproduce

  1. Have a bot account that is a member of a Matrix room whose room ID contains uppercase letters in the local-part (e.g. !ExampleMixedCase:matrix.example.org). Synapse-generated room IDs are always mixed-case, so this is the default.
  2. From the bot account, exercise an outbound code path that goes through reply routing, cron delivery, queue retry, or mirror/group send (any code path that derives the outbound to from session metadata rather than from a freshly-resolved Matrix join).
  3. Observe in gateway.err.log:
    [tools] message failed: User @bot-a:matrix.example.org not in room
      !examplemixedcase:matrix.example.org, and room previews are disabled
  4. Inspect the bot's local Matrix sync storage at <config-root>/matrix/accounts/<bot>/.../bot-storage.json. The joined-rooms list contains !ExampleMixedCase:matrix.example.org, not !examplemixedcase:matrix.example.org.
  5. Inspect the corresponding session-store entry (<config-root>/agents/<bot>/sessions/sessions.json):
    • groupId: !examplemixedcase:matrix.example.org ← lowercased
    • deliveryContext.to: room:!ExampleMixedCase:matrix.example.org ← mixed-case (correct)
    • lastTo: room:!ExampleMixedCase:matrix.example.org ← mixed-case (correct)
  6. The failed delivery-queue item (<config-root>/delivery-queue/failed/<uuid>.json) shows to: !examplemixedcase:matrix.example.org, i.e. the outbound payload used the lowercased identity even though the session preserved the mixed-case destination.

Expected behavior

Outbound Matrix sends use the provider-exact room ID — the same string the bot received when it joined the room — preserved through the entire outbound path including queue recovery, retry, and reply/mirror plumbing. The room ID Matrix issued for the room is the only valid <roomId> for POST /_matrix/client/v3/rooms/<roomId>/send/m.room.message/<txnId>.

Actual behavior

Outbound payload to field gets a lowercased identity that matches the canonicalized session/group key, not the joined room. Synapse correctly returns 403 because no room with the lowercased ID exists in that bot account's membership. The mixed-case destination is preserved in deliveryContext.to and lastTo in the session entry but is discarded somewhere in the outbound path.

OpenClaw version

2026.4.23-beta.5

Operating system

macOS 15.x (Darwin 23.x)

Install method

pnpm dev (from a moltbot checkout)

Model

n/a — this is a Matrix-routing bug; reproduces against any model.

Provider / routing chain

openclaw -> matrix-synapse (self-hosted)

Additional provider/model setup details

n/a — bug is independent of LLM provider or model. Reproduces on multiple bot accounts and multiple rooms in the same deployment.

Logs, screenshots, and evidence

7-day window, single deployment, multiple bot accounts, classified by a log scanner that splits "lowercased target" 403s from "out-of-scope target" 403s:

Class                                  Count
case-bug (lowercased mixed-case room)  357
out-of-scope (other not-in-room)        53

The 357 lowercased-target events are distributed across 5 distinct rooms and 2 distinct bot accounts. Every lowercased-target event has a matching mixed-case room in the bot's bot-storage.json joined-rooms list. The lowercased forms are not present anywhere in the bot's joined-rooms list.

Representative log line (bot/room redacted; case-loss pattern preserved):

2026-05-05T13:14:31Z [tools] message failed:
  User @bot-a:matrix.example.org not in room !examplemixedcase:matrix.example.org,
  and room previews are disabled
  raw_params={"action":"read","channel":"matrix",
             "target":"room:!examplemixedcase:matrix.example.org","limit":12}

Same case-loss pattern observed across read and send actions, and across at least these outbound contexts: cron-delivered isolated agent runs, reply-routing mirror sends, and queue-retry of previously failed sends.

Impact and severity

  • Affected: any deployment with mixed-case Matrix room IDs (i.e. essentially every Synapse-hosted deployment, since Synapse generates mixed-case room IDs).
  • Severity: behavior bug — failed deliveries surface as [tools] message failed to the agent and as queued failures in delivery-queue/failed/. No data corruption observed, no auth/security exposure. Agents experience silent message-drop in any code path that goes through the affected routing.
  • Frequency: in a deployment with normal traffic, ~50 events/day spread across multiple rooms and bots.
  • Consequence: legitimate agent-to-room messages are dropped (read attempts fail to return history; send attempts never deliver). Operators see recurring 403 noise in gateway.err.log and recurring entries in delivery-queue/failed/. Agents can also be misled into believing membership has been revoked.

Additional information

Files already audited as not-the-bug

  • extensions/matrix/src/matrix/target-ids.tsnormalizeMatrixResolvableTarget and normalizeMatrixMessagingTarget strip prefixes (matrix:, room:, channel:, user:) but do not lowercase room IDs.
  • extensions/matrix/src/matrix/send/targets.tsresolveMatrixRoomId resolves aliases and direct rooms; does not lowercase room IDs.
  • src/utils/delivery-context.shared.tsnormalizeDeliveryContext trims to via normalizeOptionalString; does not lowercase.
  • src/config/sessions/delivery-info.tsextractDeliveryInfo already has mixed-case regression coverage in its test file (preserves room:!MixedCase:example.org from stored last-route metadata and from thread-fallback).

Where the case loss is intentional

  • src/channels/session.tsrecordInboundSession() calls normalizeLowercaseStringOrEmpty(sessionKey). The lowercased session key is intentional for indexing and matching. The bug is when this lowercased identity leaks into the outbound to field.

Suspect paths to audit

The outbound path appears to mix two concepts: the lowercased canonical session/group identity (used for indexing) and the provider-exact target (used for real Matrix sends). The most suspect locations:

  • src/auto-reply/reply/route-reply.ts — passes mirror.sessionKey and groupId into outbound plumbing.
  • src/infra/outbound/message.ts
  • src/infra/outbound/deliver.ts
  • src/infra/outbound/outbound-send-service.ts
  • src/infra/outbound/delivery-queue.* — queue recovery / retry is a strong candidate (the failed queue item has to: <lowercased> while the producing session has deliveryContext.to: <mixed-case>).
  • src/infra/outbound/targets.ts
  • src/infra/outbound/targets-session.ts
  • src/cron/isolated-agent/delivery-target.ts

Suggested fix direction

Design principle for Matrix room sends: never derive the outbound room ID from a lowercased session key, groupId, or canonical identifier when a preserved exact to / nativeChannelId exists.

Field-precedence rules for outbound Matrix targets:

  1. explicit deliveryContext.to
  2. lastTo
  3. exact origin.to
  4. exact origin.nativeChannelId (re-wrap with room: prefix if needed)

Lowercased session keys, groupId, and other canonical identifiers are valid lookup keys but must not become the provider-exact outbound target.

Suggested test coverage

  1. Mixed-case Matrix room preserved through reply routing — given session key lowercased and deliveryContext.to = room:!MixedCase:example.org, the final outbound target is !MixedCase:example.org.
  2. Mixed-case room preserved through cron delivery — queued payload and recovery/retry both keep mixed-case.
  3. Mirrored / group reply — mirror.sessionKey lowercased, deliveryContext.to mixed-case, send target stays mixed-case.
  4. Queue recovery — failed queued Matrix item originally targeting !MixedCase:example.org retries against !MixedCase:example.org.
  5. Negative regression — given the joined room is !MixedCase:example.org and the lowercased variant is absent from membership, no code path may send to !mixedcase:example.org.

Why this isn't a Synapse / membership issue

The send call:

POST /_matrix/client/v3/rooms/<roomId>/send/m.room.message/<txnId>

Synapse correctly checks membership against the exact <roomId> string. Matrix room IDs are case-sensitive. The 403 is correct: the bot really isn't a member of the lowercased ID, because that's not the room ID the bot ever joined. The bug is on the OpenClaw side, in how the outbound to is derived.

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…

FAQ

Expected behavior

Outbound Matrix sends use the provider-exact room ID — the same string the bot received when it joined the room — preserved through the entire outbound path including queue recovery, retry, and reply/mirror plumbing. The room ID Matrix issued for the room is the only valid <roomId> for POST /_matrix/client/v3/rooms/<roomId>/send/m.room.message/<txnId>.

Still need to ship something?

×6

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

Back to top recommendations

TRENDING