openclaw - 💡(How to fix) Fix [Bug]: Default-mode Matrix channel rooms receive internal tool/progress events as visible chat messages

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…

Default automatic-delivery Matrix multi-person rooms can receive internal tool/progress events as visible chat messages on non-room_event turns; reproduced 2026-05-21.

Root Cause

  • #69067 — closest historical analogue: "Slack channel tool results leak despite verboseDefault=off…". The Slack-only carve-out (commit 76a4c167f7e, 2026-04-24) closing this issue is the predicate this report extends.
  • #80042 — "Fix Discord verbose tool progress delivery" (merged 2026-05-17, commit 57da466ec). Intentionally restored verbose tool progress for explicit message_tool_only channel turns. The fix here must not regress that behavior; existing test at :5211 is the regression guard.
  • #83498 — "fix(messages): keep group visible replies automatic by default" (merged 2026-05-18, commit 1e5450f23e). The trigger that made this gap visible in default operation — see Timeline below.
  • #79804 — Telegram direct chat internal-status leak (different code path, same privacy symptom class). Linked for cross-surface visibility, not as the same root cause.
  • #81892 — inverse Matrix issue: /reasoning on has no effect on Matrix channel. Same surface family.
  • #82691 — proposes messages.toolMessageLogging override. Adjacent design space; not a clean root-fix for this predicate gap.
  • #64182 — inverse intent for groups: wants tool updates allowed for authorized group command sessions. Should land consistent with this predicate change.
  • #70152 — earlier Slack-narrow fix attempt; closed without merge. Historical context only.

Fix Action

Fix / Workaround

  1. Run OpenClaw 2026.5.19 (commit 85852b5cc3 on upstream/main-tracking branch) with the Matrix channel extension bound to a multi-person Matrix room (not a DM with the bot user).
  2. Bind an agent session to that room (non-room_event inbound — i.e. an ordinary user message reply turn). Use default automatic delivery mode: no sourceReplyDeliveryMode: "message_tool_only" opt-in, no verboseLevel: "on".
  3. Trigger any tool-heavy operation: file read, search, patch apply, exec.
  4. Observe the Matrix room: internal tool-progress summaries appear as visible chat messages, e.g. 🛠️ print lines..., 🛠️ search..., 🩹 Apply Patch, 🔌 Gateway: messages.

Default-mode multi-person rooms (ChatType: "channel" or "group") should not receive internal tool summaries or verbose progress events. This matches the existing behavior for ChatType: "group" (carve-out at dispatch-from-config.ts:1231: ctx.ChatType !== "group" || ctx.IsForum === true) and for Slack non-direct (carve-out via isSlackNonDirectSurface).

The explicit message_tool_only and verboseLevel: "on" paths, restored by PR #80042, should continue to deliver verbose tool progress on channel turns. The existing test at src/auto-reply/reply/dispatch-from-config.test.ts:5211 ("delivers verbose tool progress in message-tool-only mode") encodes that contract and must stay green.

Code Example

# Symptoms observed in a private Matrix room transcript (room name and id redacted):
🛠️ print lines...
🛠️ search...
🩹 Apply Patch
🔌 Gateway: messages

---

const isSlackNonDirectSurface =
    (ctx.Surface === "slack" || ctx.Provider === "slack") && ctx.ChatType !== "direct";
  const shouldSendVerboseProgressMessages =
    !isSlackNonDirectSurface && (ctx.ChatType !== "group" || ctx.IsForum === true);
  const shouldSendToolSummaries = shouldSendVerboseProgressMessages;

---

// dispatch-from-config.ts:1228-1232
const isSlackNonDirectSurface =
  (ctx.Surface === "slack" || ctx.Provider === "slack") && ctx.ChatType !== "direct";
const chatType = normalizeChatType(ctx.ChatType);
const isMultiParticipantChat = chatType === "group" || chatType === "channel";
const isChannelExplicitMessageToolOptIn =
  chatType === "channel" && sourceReplyDeliveryMode === "message_tool_only";
const shouldSendVerboseProgressMessages =
  !isSlackNonDirectSurface &&
  (!isMultiParticipantChat || ctx.IsForum === true || isChannelExplicitMessageToolOptIn);
const shouldSendToolSummaries = shouldSendVerboseProgressMessages;

---

export function isMultiParticipantChatType(raw?: string): boolean {
  const chatType = normalizeChatType(raw);
  return chatType === "group" || chatType === "channel";
}

---

// Minimal failing-case regression (DEFAULT automatic mode, non-room_event)
it("suppresses text-only tool summaries on default-mode non-room_event Matrix channel turns", async () => { /* … */ });

// Contract preservation (mirrors :5211 for Matrix)
it("preserves PR #80042 behavior: message_tool_only + verbose:on still delivers tool progress on Matrix channel", async () => { /* … */ });

// Default-mode verbose plan/status suppression
it("suppresses verbose plan/status updates on default-mode Matrix channel turns", async () => { /* … */ });

// Discord channel default-mode non-room_event (complements :2243 and :5211)
it("suppresses tool summaries on default-mode Discord channel turns (non-room_event)", async () => { /* … */ });

// reply_dispatch hook contract
it("emits reply_dispatch hook with shouldSendToolSummaries=false for default-mode channel rooms", async () => { /* … */ });

// Pass-through guard
it("forwards media-only tool payloads and exec-approval payloads in channel rooms", async () => { /* … */ });

// Helper unit test
it("isMultiParticipantChatType handles direct/dm/group/channel/casing/whitespace/unknown", () => { /* … */ });
RAW_BUFFERClick to expand / collapse

Bug type

Regression (worked before, now fails)

Beta release blocker

No

Summary

Default automatic-delivery Matrix multi-person rooms can receive internal tool/progress events as visible chat messages on non-room_event turns; reproduced 2026-05-21.

Steps to reproduce

  1. Run OpenClaw 2026.5.19 (commit 85852b5cc3 on upstream/main-tracking branch) with the Matrix channel extension bound to a multi-person Matrix room (not a DM with the bot user).
  2. Bind an agent session to that room (non-room_event inbound — i.e. an ordinary user message reply turn). Use default automatic delivery mode: no sourceReplyDeliveryMode: "message_tool_only" opt-in, no verboseLevel: "on".
  3. Trigger any tool-heavy operation: file read, search, patch apply, exec.
  4. Observe the Matrix room: internal tool-progress summaries appear as visible chat messages, e.g. 🛠️ print lines..., 🛠️ search..., 🩹 Apply Patch, 🔌 Gateway: messages.

Expected behavior

Default-mode multi-person rooms (ChatType: "channel" or "group") should not receive internal tool summaries or verbose progress events. This matches the existing behavior for ChatType: "group" (carve-out at dispatch-from-config.ts:1231: ctx.ChatType !== "group" || ctx.IsForum === true) and for Slack non-direct (carve-out via isSlackNonDirectSurface).

The explicit message_tool_only and verboseLevel: "on" paths, restored by PR #80042, should continue to deliver verbose tool progress on channel turns. The existing test at src/auto-reply/reply/dispatch-from-config.test.ts:5211 ("delivers verbose tool progress in message-tool-only mode") encodes that contract and must stay green.

Actual behavior

Default-mode Matrix channel turns (ChatType: "channel", no message_tool_only, no verboseLevel: "on", non-room_event) emit internal tool/progress events into the visible Matrix chat surface. Observed 2026-05-21 in a private Matrix room (name redacted) during ordinary investigation work (file reads, search, patch apply).

OpenClaw version

2026.5.19 (build 85852b5cc3c0c39086a27c7df6c14576a48ddd0f, built 2026-05-20T17:11Z).

The reproduced deployment tracks upstream/main with a small set of unrelated local patches (audience field for hidden runtime context, trajectory null-safety which is now also in main via #84797). None of the local patches touch dispatch-from-config.ts, extensions/matrix/, or the verbose-progress / tool-summary code paths — verified by git log --name-only upstream/main..mercury/local-patches.

Operating system

macOS 14.7.6 (Darwin 23.6.0, x86_64).

Install method

pnpm dev build (pnpm build against the tracking branch; daemon launched via /Library/LaunchDaemons/ai.openclaw.gateway.plist).

Model

NOT_ENOUGH_INFO

(The bug is in dispatch-context routing logic, not model-dependent; the specific model for the leaked turn was not captured.)

Provider / routing chain

openclaw → matrix extension (extensions/matrix) → Matrix homeserver (multi-person room).

Inbound classification: extensions/matrix/src/matrix/monitor/handler.ts:1351 emits ChatType: isDirectMessage ? "direct" : "channel". Outbound session routing: extensions/matrix/src/session-route.ts:103 emits chatType = target.kind === "user" ? "direct" : "channel". Both verified on upstream/main.

Additional provider/model setup details

No relevant agent/model overrides for this case — the bug surfaces at dispatch context routing before any model call. Default effectiveVisibleReplies (none of automatic / message_tool was explicitly set on the affected session). No safe-debug-room configuration enabled.

Logs, screenshots, and evidence

# Symptoms observed in a private Matrix room transcript (room name and id redacted):
🛠️ print lines...
🛠️ search...
🩹 Apply Patch
🔌 Gateway: messages

Source-level evidence (verifiable on upstream/main at the time of writing):

  • The predicate gap: src/auto-reply/reply/dispatch-from-config.ts:1228-1232

    const isSlackNonDirectSurface =
      (ctx.Surface === "slack" || ctx.Provider === "slack") && ctx.ChatType !== "direct";
    const shouldSendVerboseProgressMessages =
      !isSlackNonDirectSurface && (ctx.ChatType !== "group" || ctx.IsForum === true);
    const shouldSendToolSummaries = shouldSendVerboseProgressMessages;

    For Matrix ChatType: "channel": isSlackNonDirectSurface is false (not Slack) and ctx.ChatType !== "group" is true ("channel""group"), so verbose progress and tool summaries are allowed.

  • Matrix emits "channel" for non-DMs: extensions/matrix/src/matrix/monitor/handler.ts:1351, extensions/matrix/src/session-route.ts:103.

  • ChatType is typed as "direct" | "group" | "channel" in src/channels/chat-type.ts; normalizeChatType() is already exported.

  • Sibling files already pair "group" and "channel" correctly: src/auto-reply/reply/source-reply-delivery-mode.ts:59, 65; completion-delivery-policy.ts:64; agent-runner-execution.ts:486; reply-threading.ts:23; get-reply-run.ts:109, 691; src/auto-reply/dispatch.ts:132. The predicate at dispatch-from-config.ts:1230 is the outlier.

  • Existing contract that the fix must preserve: src/auto-reply/reply/dispatch-from-config.test.ts:5211 requires ChatType: "channel" + sourceReplyDeliveryMode: "message_tool_only" + verboseLevel: "on" to deliver verbose tool progress. Restored by PR #80042 (commit 57da466ec).

  • Existing room_event suppression at dispatch-from-config.test.ts:2243 covers ambient room-event turns and should keep passing.

Impact and severity

  • Affected: Sessions bound to multi-person Matrix rooms in default automatic-delivery mode. Source-level admission also extends to non-Slack channel-class surfaces (Discord, Mattermost, MS Teams, Google Chat, WhatsApp newsletter, Synology Chat, ClickClack, Feishu, QA channel — none separately reproduced).
  • Severity: Privacy-sensitive. Internal tool/progress summaries can carry local file paths, command names, search terms, and operationally sensitive context. Maintainers to decide on impact:security label.
  • Frequency: Observed deterministically on the affected setup whenever a tool-heavy operation runs on a default-mode Matrix channel turn (single repro environment; not separately measured across deployments).
  • Consequence: Privacy exposure in the visible chat surface; operational noise; risk of leaking implementation details to non-operator participants.

Additional information

Cross-references (state and labels should be re-confirmed by triage)

  • #69067 — closest historical analogue: "Slack channel tool results leak despite verboseDefault=off…". The Slack-only carve-out (commit 76a4c167f7e, 2026-04-24) closing this issue is the predicate this report extends.
  • #80042 — "Fix Discord verbose tool progress delivery" (merged 2026-05-17, commit 57da466ec). Intentionally restored verbose tool progress for explicit message_tool_only channel turns. The fix here must not regress that behavior; existing test at :5211 is the regression guard.
  • #83498 — "fix(messages): keep group visible replies automatic by default" (merged 2026-05-18, commit 1e5450f23e). The trigger that made this gap visible in default operation — see Timeline below.
  • #79804 — Telegram direct chat internal-status leak (different code path, same privacy symptom class). Linked for cross-surface visibility, not as the same root cause.
  • #81892 — inverse Matrix issue: /reasoning on has no effect on Matrix channel. Same surface family.
  • #82691 — proposes messages.toolMessageLogging override. Adjacent design space; not a clean root-fix for this predicate gap.
  • #64182 — inverse intent for groups: wants tool updates allowed for authorized group command sessions. Should land consistent with this predicate change.
  • #70152 — earlier Slack-narrow fix attempt; closed without merge. Historical context only.

Timeline (analysis, not direct observation)

The prefersMessageToolDelivery masking story is analysis based on diff inspection; the precise "what changed in default visibility" should be confirmed by a pre/post dispatch-from-config.test.ts run, which has not been performed for this report.

DateCommit / PREffect
2026-04-2476a4c167f7e (closes #69067, "fix(slack): suppress verbose progress in rooms")Introduces predicate ctx.ChatType !== "group" || ctx.IsForum === true. Slack carve-out only. Latent gap for non-Slack channel surfaces.
2026-04-24 → 2026-05-18Gap may have been masked in default operation by prefersMessageToolDelivery auto-allowing the message tool for group/channel turns; precise interaction unverified.
2026-05-1757da466ec / PR #80042Intentionally restored verbose tool progress for explicit message_tool_only channel turns. Any fix here must preserve this.
2026-05-181e5450f23e / PR #83498Removed the channel-aware prefersMessageToolDelivery clause. Predicate gap visible in default-mode non-room_event turns on channel-class surfaces.
2026-05-21Reproduced on Matrix during tool-heavy agent work in default automatic delivery mode.

Surfaces that source-admit ChatType: "channel"

Known examples on upstream/main (not exhaustive; source-suspect, not separately reproduced; Slack non-DM is already covered by the isSlackNonDirectSurface guard):

  • Discord: extensions/discord/src/monitor/message-handler.process.ts:212-219; inbound envelope at message-handler.context.ts:333-358. Existing room_event test at :2243 and message_tool_only test at :5211 constrain the fix.
  • Mattermost: extensions/mattermost/src/session-route.ts:38, extensions/mattermost/src/mattermost/monitor-auth.ts:257, extensions/mattermost/src/mattermost/slash-http.ts:463.
  • MS Teams: extensions/msteams/src/session-route.ts:32, monitor-handler/message-handler.ts:790.
  • Google Chat: extensions/googlechat/src/monitor.ts:335.
  • WhatsApp: extensions/whatsapp/src/session-route.ts:18 (newsletter targets emit "channel").
  • Synology Chat: extensions/synology-chat/src/inbound-event.ts:91.
  • ClickClack: extensions/clickclack/src/inbound.ts:22, 38, channel.ts:125.
  • Feishu: advertises chatTypes: ["direct", "channel"] at extensions/feishu/src/channel.ts:682.
  • QA channel: extensions/qa-channel/src/bus-client.ts:138, 145, channel-actions.ts:62, 71.
  • QQ bot: wider c2c|group|guild|dm|channel alternation at extensions/qqbot/src/engine/utils/request-context.ts:33; benefits from normalization at the dispatch boundary.

Shared inbound builder at src/channels/inbound-event/context.ts:194 passes params.conversation.kind through as ChatType.

Suggested fix shape (proposal, not part of the bug report)

The desired policy:

  • Slack non-direct: suppress (existing behavior; preserved by isSlackNonDirectSurface).
  • ChatType: "group": suppress unless IsForum === true (existing behavior).
  • ChatType: "channel": suppress by default (NEW — this is the bug fix) unless IsForum === true or sourceReplyDeliveryMode === "message_tool_only" (preserves PR #80042's explicit-mode contract verified by dispatch-from-config.test.ts:5211).
  • ChatType: "direct": allow (existing behavior).

Note on the cascade: dispatch-from-config.ts:1234-1240 defines shouldDeliverVerboseProgressDespiteSourceSuppression which is itself gated on shouldSendVerboseProgressMessages. Any change that makes the predicate return false for channel unconditionally would also kill the explicit-mode opt-in path. The proposal below threads sourceReplyDeliveryMode into the predicate to keep that path intact.

Concrete shape (illustrative; maintainers may prefer a different factoring):

// dispatch-from-config.ts:1228-1232
const isSlackNonDirectSurface =
  (ctx.Surface === "slack" || ctx.Provider === "slack") && ctx.ChatType !== "direct";
const chatType = normalizeChatType(ctx.ChatType);
const isMultiParticipantChat = chatType === "group" || chatType === "channel";
const isChannelExplicitMessageToolOptIn =
  chatType === "channel" && sourceReplyDeliveryMode === "message_tool_only";
const shouldSendVerboseProgressMessages =
  !isSlackNonDirectSurface &&
  (!isMultiParticipantChat || ctx.IsForum === true || isChannelExplicitMessageToolOptIn);
const shouldSendToolSummaries = shouldSendVerboseProgressMessages;

Optional shared helper alongside normalizeChatType() in src/channels/chat-type.ts:

export function isMultiParticipantChatType(raw?: string): boolean {
  const chatType = normalizeChatType(raw);
  return chatType === "group" || chatType === "channel";
}

Critical constraints for the fix:

  • isSlackNonDirectSurface guard stays (catches missing/unknown ChatType on Slack non-direct that a strict normalizeChatType-based helper would not).
  • Forum carve-out preserved.
  • Must keep dispatch-from-config.test.ts:5211 green (PR #80042's ChatType: "channel" + sourceReplyDeliveryMode: "message_tool_only" + verboseLevel: "on" contract). The proposal above includes isChannelExplicitMessageToolOptIn specifically to preserve that path so the cascade at :1234-1240 still fires.
  • Note the asymmetry vs group: explicit message_tool_only opt-in keeps verbose progress on channel (matches PR #80042) but does NOT on group (matches current behavior — not changed by PR #80042). If maintainers want symmetric behavior, that's a separate decision and should be called out in the fix PR.
  • Predicate stays inside the strict "direct" | "group" | "channel" ChatType contract.

Suggested new tests (for the fix PR, not part of this issue)

// Minimal failing-case regression (DEFAULT automatic mode, non-room_event)
it("suppresses text-only tool summaries on default-mode non-room_event Matrix channel turns", async () => { /* … */ });

// Contract preservation (mirrors :5211 for Matrix)
it("preserves PR #80042 behavior: message_tool_only + verbose:on still delivers tool progress on Matrix channel", async () => { /* … */ });

// Default-mode verbose plan/status suppression
it("suppresses verbose plan/status updates on default-mode Matrix channel turns", async () => { /* … */ });

// Discord channel default-mode non-room_event (complements :2243 and :5211)
it("suppresses tool summaries on default-mode Discord channel turns (non-room_event)", async () => { /* … */ });

// reply_dispatch hook contract
it("emits reply_dispatch hook with shouldSendToolSummaries=false for default-mode channel rooms", async () => { /* … */ });

// Pass-through guard
it("forwards media-only tool payloads and exec-approval payloads in channel rooms", async () => { /* … */ });

// Helper unit test
it("isMultiParticipantChatType handles direct/dm/group/channel/casing/whitespace/unknown", () => { /* … */ });

Cross-surface coverage can be parameterized on Provider rather than duplicated per surface.

Operational workaround (current Mercury deployment)

Until merged: avoid running tool-heavy investigations from a session bound to a multi-person Matrix room in default-mode delivery. Use a DM, the web dashboard, or a dedicated safe room; return only a final summary to the public room.

Suggested labels

bug, bug:behavior, regression, matrix, gateway. Maintainers to decide on P1 and impact:security given the evidence is source-level plus one Matrix repro.

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

Default-mode multi-person rooms (ChatType: "channel" or "group") should not receive internal tool summaries or verbose progress events. This matches the existing behavior for ChatType: "group" (carve-out at dispatch-from-config.ts:1231: ctx.ChatType !== "group" || ctx.IsForum === true) and for Slack non-direct (carve-out via isSlackNonDirectSurface).

The explicit message_tool_only and verboseLevel: "on" paths, restored by PR #80042, should continue to deliver verbose tool progress on channel turns. The existing test at src/auto-reply/reply/dispatch-from-config.test.ts:5211 ("delivers verbose tool progress in message-tool-only mode") encodes that contract and must stay green.

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 [Bug]: Default-mode Matrix channel rooms receive internal tool/progress events as visible chat messages