Skip to content

fix(matrix): fix accountId not forwarded in sendMessage tool action#28399

Closed
psacc wants to merge 2 commits into
openclaw:mainfrom
psacc:fix/matrix-sendmessage-accountid
Closed

fix(matrix): fix accountId not forwarded in sendMessage tool action#28399
psacc wants to merge 2 commits into
openclaw:mainfrom
psacc:fix/matrix-sendmessage-accountid

Conversation

@psacc

@psacc psacc commented Feb 27, 2026

Copy link
Copy Markdown

AI-assisted, human-driven. Fix identified, reviewed, tested, and submitted by a human engineer. Implementation and tests written with Claude Code (Claude Sonnet 4.6).

Summary

  • Problem: In isolated sessions (cron jobs, subagents), the message tool drops
    accountId at three hops before it reaches resolveMatrixClient. The field is
    accepted by the tool schema but silently lost in the adapter chain.
  • Why it matters: Multi-account gateways always fall through to
    getAnyActiveMatrixClient(), which returns the first account by Map insertion order —
    non-deterministic across restarts. In practice: wrong account sends the message,
    M_FORBIDDEN if the room is account-restricted, silent false-positive
    deliveryStatus: "delivered" even when the message was dropped.
  • What changed: Three hops fixed across 6 files (~60 lines production code).
    All Matrix tool actions now correctly route through the agent's bound account.
    Also fixes a reactMatrixMessage call site in monitor/handler.ts, adds structured
    send logging to sendMessageMatrix, and adds a warn to getAnyActiveMatrixClient
    when called with multiple active accounts.
  • What did NOT change: No schema changes, no behaviour change for single-account
    gateways (accountId undefined → same resolveMatrixClient fallback as before).

Three hops fixed

Hop 1 — actions.ts (adapter layer, root cause)

matrixMessageActions.handleAction receives ctx.accountId (server-injected binding
account for the current session) but never included it when building params for
handleMatrixAction. Discord's equivalent adapter already had the correct pattern:
ctx.accountId ?? readStringParam(params, "accountId"). Matrix was missing it entirely.

Hop 2 — tool-actions.ts (dispatch layer)

accountId was not extracted from params for editMessage, deleteMessage,
readMessages, react, reactions, pinMessage, unpinMessage, listPins.
Only sendMessage had it (fixed in the first commit on this branch).

Hop 3 — actions/messages.ts (send layer)

sendMatrixMessage accepted opts.accountId but did not forward it to
sendMessageMatrix.

Change Type

  • Bug fix

Scope

  • Skills / tool execution
  • Integrations

Linked Issue/PR

User-visible / Behavior Changes

In multi-account Matrix setups, all message tool actions from isolated cron/subagent
sessions now route to the account specified by accountId (or the agent's bound
account via ctx.accountId) instead of an arbitrary active account.

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS
  • Runtime: Node 22.x
  • Integration: Matrix, multi-account gateway (≥2 accounts)
  • Config: isolated cron session, sessionTarget: "isolated", agent with a specific
    bound Matrix account; target room accessible only to that agent's account

Steps

  1. Configure a multi-account Matrix gateway (≥2 accounts)
  2. Create an isolated cron job for a non-default agent targeting a Matrix room where
    only that agent's account is a member
  3. Trigger the cron — the message tool routes to an arbitrary account, returns
    M_FORBIDDEN
  4. Apply this fix; retrigger — routes correctly to the agent's bound account

Expected

Message sent from the agent's own Matrix account. No M_FORBIDDEN.

Actual (before fix)

Sent from the first account in activeClients Map by startup order (non-deterministic).
M_FORBIDDEN if that account is not a member of the target room.

Evidence

Before — wrong account used (Map insertion order), not in target room:

M_FORBIDDEN: User @<wrong-account>:matrix.org not in room !<room>:matrix.org

After — fix applied, different account still first in Map at startup:

[matrix:send] account=<agent-account> room=!<room>:matrix.org
[matrix:send:ok] account=<agent-account> room=!<room>:matrix.org eventId=...

Correct account used regardless of startup order. ✅

  • Failing log before + passing after (gateway live test)
  • Unit tests: 110/110 passing (26 test files in Matrix extension)

Human Verification

  • Verified: isolated session on a multi-account gateway; a different account completed
    startup first; message still routed to the correct agent account.
  • Verified: all tool actions (sendMessage, editMessage, deleteMessage,
    readMessages, react, reactions, pinMessage, unpinMessage, listPins)
    now forward accountId correctly.
  • Edge cases: single-account gateway — accountId is undefined, falls through to
    existing resolveMatrixClient default logic unchanged.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery

  • Revert changes in actions.ts, tool-actions.ts, matrix/actions/messages.ts
  • Behaviour reverts to pre-fix (arbitrary account selection)
  • Symptom to watch for: M_FORBIDDEN from multi-account isolated sessions

Risks and Mitigations

  • Risk: callers that previously relied on getAnyActiveMatrixClient() as an implicit
    default (no accountId in params) now go through resolveMatrixClient(undefined)
    instead.
    • Mitigation: resolveMatrixClient(undefined) already has its own fallback chain —
      behaviour is identical for callers that omit accountId. Single-account setups:
      no change.

@openclaw-barnacle openclaw-barnacle Bot added channel: matrix Channel integration: matrix size: S labels Feb 27, 2026
@psacc psacc marked this pull request as ready for review February 27, 2026 06:43
@greptile-apps

greptile-apps Bot commented Feb 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a critical bug in multi-account Matrix setups where the sendMessage tool action was not forwarding the accountId parameter to the Matrix client resolver. This caused isolated sessions (cron jobs, subagents) to use an arbitrary account (first by Map insertion order) instead of the intended account, resulting in M_FORBIDDEN errors when the wrong account tried to send messages to restricted rooms.

The fix is minimal and surgical:

  • Extracts accountId from params in tool-actions.ts (line 85)
  • Forwards it through sendMatrixMessage in matrix/actions/messages.ts (line 30)
  • Adds comprehensive regression tests covering both multi-account and single-account scenarios
  • Updates CHANGELOG with clear explanation of the issue and fix

The implementation maintains backward compatibility by using the ?? undefined pattern to normalize null/undefined values, ensuring single-account setups continue to work unchanged. The PR correctly scopes the fix to sendMessage only, acknowledging that other tool actions (editMessage, deleteMessage, etc.) have the same pattern but will be addressed separately per issue #26457.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk - it's a focused bug fix with clear scope and comprehensive test coverage.
  • The fix is minimal (3 lines), well-tested with regression coverage, maintains backward compatibility, and solves a real production bug. The implementation follows existing patterns in the codebase (using ?? undefined for optional param normalization), type safety is preserved, and the scope is appropriately limited to prevent scope creep. The PR description is thorough with clear reproduction steps and verification evidence.
  • No files require special attention - all changes are straightforward parameter forwarding with appropriate test coverage.

Last reviewed commit: 439da7e

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 439da7e39e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@@ -82,10 +82,12 @@ export async function handleMatrixAction(
const replyToId =
readStringParam(params, "replyToId") ?? readStringParam(params, "replyTo");
const threadId = readStringParam(params, "threadId");
const accountId = readStringParam(params, "accountId");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Forward accountId from the send adapter path

handleMatrixAction now reads accountId from params, but the normal Matrix send flow still drops that field before this point: extensions/matrix/src/actions.ts builds a new object for handleMatrixAction with to/content/mediaUrl/replyToId/threadId only. In tool-driven sends (action: "send"), accountId therefore remains undefined here and sendMatrixMessage still falls back to default/any active client in multi-account setups, so this fix does not actually resolve the misrouting scenario it targets.

Useful? React with 👍 / 👎.

@psacc psacc force-pushed the fix/matrix-sendmessage-accountid branch from 07d4dad to 50e5186 Compare February 27, 2026 21:33
psacc and others added 2 commits February 27, 2026 22:47
In multi-account Matrix setups, all tool actions from isolated cron and
subagent sessions were routing through getAnyActiveMatrixClient() —
returning the first account by Map insertion order, non-deterministic
across gateway restarts — instead of the agent's bound account. This
caused M_FORBIDDEN errors when the wrong account tried to send to a
room it was not a member of, with a false-positive deliveryStatus of
"delivered".

Three hops fixed:

1. actions.ts — matrixMessageActions.handleAction received ctx.accountId
   (server-injected binding account) but never forwarded it when building
   params for handleMatrixAction. Discord's equivalent adapter already had
   the correct pattern; Matrix was missing it.

2. tool-actions.ts — accountId was not extracted from params for
   editMessage, deleteMessage, readMessages, react, reactions, pinMessage,
   unpinMessage, listPins. Only sendMessage had it.

3. actions/messages.ts — sendMatrixMessage accepted opts.accountId but
   did not forward it to sendMessageMatrix.

Also fixes a reactMatrixMessage signature inconsistency (bare client?
param replaced with opts: {client?, accountId?}) and a broken call site
in monitor/handler.ts that would have sent reaction acknowledgements from
the wrong account. Adds structured send logging to sendMessageMatrix
([matrix:send] / [matrix:send:ok] / [matrix:send:fail]) and a warn to
getAnyActiveMatrixClient when called with multiple active accounts.

Closes openclaw#26457

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…w#26457)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@psacc psacc force-pushed the fix/matrix-sendmessage-accountid branch from 63f3680 to 0ef1d4a Compare February 27, 2026 21:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0ef1d4a9b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

action: "memberInfo",
userId,
roomId: readStringParam(params, "roomId") ?? readStringParam(params, "channelId"),
accountId: accountId ?? undefined,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate accountId for member/channel info lookups

matrixMessageActions.handleAction now adds accountId for member-info/channel-info, but handleMatrixAction still calls getMatrixMemberInfo and getMatrixRoomInfo without passing that field (extensions/matrix/src/tool-actions.ts), so multi-account sessions can still resolve the wrong Matrix client and return unauthorized/incorrect room/member metadata. This leaves those actions with the same misrouting behavior the commit is fixing for message/reaction/pin actions.

Useful? React with 👍 / 👎.

@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Mar 5, 2026
@gumadeiras

Copy link
Copy Markdown
Member

OpenClaw now uses a new Matrix plugin built on the official matrix-js-sdk. The older Matrix implementation is no longer supported.

Closing this as potentially fixed in the new plugin. If this still reproduces after migrating, please open a new issue/PR against the new Matrix plugin and link back here.

Migration/docs:
https://docs.openclaw.ai/channels/matrix
https://docs.openclaw.ai/install/migrating-matrix

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: matrix Channel integration: matrix size: L stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Matrix multi-account: tool-action message may send from wrong account

2 participants