openclaw - 💡(How to fix) Fix [Bug]: Discord mentionAliases is not applied to session reply / final assistant text, only to message tool calls

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…

channels.discord.mentionAliases deterministically rewrites @handle<@userId> for outbound Discord messages, but only when the message is sent via the message tool. When an agent produces a natural-language reply (the assistant's final text output, sent to the originating Discord channel as a session reply), mentionAliases is not applied — the text reaches Discord verbatim, so @TeammateName stays as plain text and the target user is never pinged.

This breaks the headline use case the feature was designed for: multi-agent Discord deployments where agents need to reliably notify other agents.

Root Cause

This is exactly the scenario mentionAliases was introduced for (see #67587). In a multi-agent setup, agents need to escalate to teammates by writing @Vladislava (or any configured handle). For users running 10+ agent bots in shared Discord channels, the failure mode is:

  • Agent A's tool call (message) → @Vladislava → rewritten to <@USER_ID> → real Discord ping ✅
  • Agent A's natural reply text → @Vladislava → sent as-is → plain text, no ping ❌

This means the collaboration link is broken exactly when the agent reasons naturally about the workflow rather than explicitly invoking the message tool. In practice, that's the majority of inter-agent communications because:

  1. The model often integrates a teammate notification into its conclusion ("Done. @Vladislava please review.") rather than splitting it into a separate tool call.
  2. Adding rules to SOUL/system prompt to force <@USER_ID> syntax is unreliable; models drift back to natural @name style under load.
  3. Splitting every cross-agent mention into a discrete message tool call doubles the outbound message count and degrades UX.

Fix Action

Fix / Workaround

The literal string @Sentinel ping test reaches Discord. It renders as plain text — no mention, no notification, no entry in message.mentions. The session reply text never appears in any plugin hook log either (message_sending, before_dispatch, reply_dispatch are all silent for this code path).

3. Plugin hooks don't fill the gap either. I registered a plugin with before_tool_call, message_sending, before_dispatch, and reply_dispatch handlers. before_tool_call fires on the message-tool path (as expected). The other three do not fire for session reply text — confirmed by logging every invocation and grepping gateway.log for the verbatim reply text after a controlled test. The session reply bypasses the hook surface entirely on the natural assistant-text dispatch path.

If a full wire-level rewriter is too invasive, the smallest useful improvement is to surface session-reply text through message_sending / before_dispatch consistently, so plugins can do the rewriting themselves. Currently those hooks are silent on this path.

Code Example

{
     "channels": {
       "discord": {
         "mentionAliases": {
           "Sentinel": "1485891428809707651"
         },
         "accounts": { "alchemist": { ... }, "sentinel": { ... } }
       }
     }
   }

---

agentPrompt: {
  messageToolHints: () => [
    "- Discord mentions: use canonical outbound syntax: users `<@USER_ID>`, channels `<#CHANNEL_ID>`, and roles `<@&ROLE_ID>`. Plain `@name` text only pings when a configured `mentionAliases` entry rewrites it; do not use the legacy `<@!USER_ID>` nickname form.",
    ...
  ]
}

---

function stripDiscordInternalRuntimeScaffolding(text: string): string {
  return text
    .replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE, "")
    .replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE, "")
    .replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE, "");
}

export const discordOutbound: ChannelOutboundAdapter = {
  ...
  sanitizeText: ({ text }) => stripDiscordInternalRuntimeScaffolding(text),
  ...
  sendText: async ({ cfg, to, text, ... }) => {
    // text is passed straight to sendMessageDiscord — no mention rewriting
    return await send(resolveDiscordOutboundTarget({ to, threadId }), text, { ... });
  },
};

---

{
     "channels": {
       "discord": {
         "mentionAliases": {
           "Sentinel": "1485891428809707651"
         },
         "accounts": { "alchemist": { ... }, "sentinel": { ... } }
       }
     }
   }

---
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

[Bug]: Discord mentionAliases is not applied to session reply / final assistant text, only to message tool calls

Summary

channels.discord.mentionAliases deterministically rewrites @handle<@userId> for outbound Discord messages, but only when the message is sent via the message tool. When an agent produces a natural-language reply (the assistant's final text output, sent to the originating Discord channel as a session reply), mentionAliases is not applied — the text reaches Discord verbatim, so @TeammateName stays as plain text and the target user is never pinged.

This breaks the headline use case the feature was designed for: multi-agent Discord deployments where agents need to reliably notify other agents.

Why this matters

This is exactly the scenario mentionAliases was introduced for (see #67587). In a multi-agent setup, agents need to escalate to teammates by writing @Vladislava (or any configured handle). For users running 10+ agent bots in shared Discord channels, the failure mode is:

  • Agent A's tool call (message) → @Vladislava → rewritten to <@USER_ID> → real Discord ping ✅
  • Agent A's natural reply text → @Vladislava → sent as-is → plain text, no ping ❌

This means the collaboration link is broken exactly when the agent reasons naturally about the workflow rather than explicitly invoking the message tool. In practice, that's the majority of inter-agent communications because:

  1. The model often integrates a teammate notification into its conclusion ("Done. @Vladislava please review.") rather than splitting it into a separate tool call.
  2. Adding rules to SOUL/system prompt to force <@USER_ID> syntax is unreliable; models drift back to natural @name style under load.
  3. Splitting every cross-agent mention into a discrete message tool call doubles the outbound message count and degrades UX.

Steps to reproduce

  1. Set up two Discord bot accounts under channels.discord.accounts (e.g. alchemist, sentinel), both sharing a guild/channel.
  2. Configure outbound aliases:
    {
      "channels": {
        "discord": {
          "mentionAliases": {
            "Sentinel": "1485891428809707651"
          },
          "accounts": { "alchemist": { ... }, "sentinel": { ... } }
        }
      }
    }
  3. Have alchemist produce a natural reply that contains @Sentinel ping test (i.e. let the LLM emit this in its final text, not via a message tool call).
  4. Observe the message arriving in Discord.

Expected behavior

@Sentinel is rewritten to <@1485891428809707651> before the outbound send, so Sentinel receives a real Discord ping.

Actual behavior

The literal string @Sentinel ping test reaches Discord. It renders as plain text — no mention, no notification, no entry in message.mentions. The session reply text never appears in any plugin hook log either (message_sending, before_dispatch, reply_dispatch are all silent for this code path).

In contrast, if the same message is sent via the message tool (message({channel:"discord", to:"channel:<id>", message:"@Sentinel ping test"})), it is correctly rewritten to <@1485891428809707651> and pings.

Root cause (source-level analysis)

I traced the path in main and the rewriter only lives on the message-tool action handler, not on the wire-level outbound adapter:

1. extensions/discord/src/channel.ts — the agent prompt hint explicitly tells the model that mentionAliases only covers the tool path:

agentPrompt: {
  messageToolHints: () => [
    "- Discord mentions: use canonical outbound syntax: users `<@USER_ID>`, channels `<#CHANNEL_ID>`, and roles `<@&ROLE_ID>`. Plain `@name` text only pings when a configured `mentionAliases` entry rewrites it; do not use the legacy `<@!USER_ID>` nickname form.",
    ...
  ]
}

The hint is registered under messageToolHints, scoped to message-tool invocations.

2. extensions/discord/src/outbound-adapter.tssanitizeText on the wire-level outbound only strips <system-reminder> / <previous_response> tags. No mentionAliases consultation:

function stripDiscordInternalRuntimeScaffolding(text: string): string {
  return text
    .replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_BLOCK_RE, "")
    .replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_SELF_CLOSING_RE, "")
    .replace(DISCORD_INTERNAL_RUNTIME_SCAFFOLDING_TAG_RE, "");
}

export const discordOutbound: ChannelOutboundAdapter = {
  ...
  sanitizeText: ({ text }) => stripDiscordInternalRuntimeScaffolding(text),
  ...
  sendText: async ({ cfg, to, text, ... }) => {
    // text is passed straight to sendMessageDiscord — no mention rewriting
    return await send(resolveDiscordOutboundTarget({ to, threadId }), text, { ... });
  },
};

A grep -r "mentionAliases" extensions/discord/src/ confirms it is consumed only inside the message-tool prepareSendPayload / handleAction chain, not in outbound-adapter.ts or sendText.

3. Plugin hooks don't fill the gap either. I registered a plugin with before_tool_call, message_sending, before_dispatch, and reply_dispatch handlers. before_tool_call fires on the message-tool path (as expected). The other three do not fire for session reply text — confirmed by logging every invocation and grepping gateway.log for the verbatim reply text after a controlled test. The session reply bypasses the hook surface entirely on the natural assistant-text dispatch path.

Suggested fix

Wire mentionAliases rewriting into the wire-level outbound, so it covers every outbound payload regardless of which agent code path produced the text:

  • Option A (smallest change): extend sanitizeText in extensions/discord/src/outbound-adapter.ts to also apply mentionAliases rewriting after the <system-reminder> strip. The rewriter has access to cfg via the outbound context.
  • Option B (cleaner separation): add a new rewriteOutbound stage to ChannelOutboundAdapter that runs after sanitizeText and before sendText. Discord's implementation pulls cfg.channels.discord.mentionAliases (+ per-account overrides) and rewrites @handle<@id> for all handles in the configured map. Same logic that currently lives in discordMessageActions.prepareSendPayload, factored out and shared.

In both options, keep the existing rules (@everyone / @here / mentions inside code spans are left alone, unknown handles are left alone).

If a full wire-level rewriter is too invasive, the smallest useful improvement is to surface session-reply text through message_sending / before_dispatch consistently, so plugins can do the rewriting themselves. Currently those hooks are silent on this path.

Workarounds we tried (FYI for anyone hitting this)

  1. ❌ Setting channels.discord.mentionAliases at top level or per-account — config is loaded (hot reload applies) but session reply never sees the rewriter.
  2. ❌ Switching streaming.mode from progressoff to force "normal" dispatch — no effect, the rewriter isn't on the normal-dispatch path either.
  3. ❌ Plugin with message_sending / before_dispatch / reply_dispatch hooks — none of them fire for session reply text. Only before_tool_call fires (message-tool path).
  4. ⚠️ Strengthening SOUL.md / system prompt to require <@USER_ID> directly — works most of the time but unreliable under load; models drift to plain @name.
  5. ⚠️ Forcing all cross-agent mentions through the message tool — works but doubles message count and degrades UX (user sees agent's reply + a separate tool message).

Environment

  • OpenClaw version: 2026.5.12
  • OS: macOS (Mac Studio)
  • Channel: Discord
  • Setup: 17 Discord bot accounts in a personal multi-agent deployment

Related issues

  • #67587 — original feature request that introduced mentionAliases (config-side only)
  • #78077 — Native outbound message finalizer (overlaps in the "wire-level outbound rewriter" space)
  • #15147 — Reply ordering race between message tool and post-turn final reply (related: confirms there are two independent outbound paths)
  • #44309 — One-way dispatch mode for A2A handoffs (related multi-agent UX issue)

Why filing this

mentionAliases is documented and configurable as if it applies to all Discord outbound; the multi-agent example in the docs (molty writing @Mantis) reads as a wire-level guarantee. In practice today it's a tool-path-only guarantee, which is the opposite of where most cross-agent mentions actually originate (natural assistant text). Closing this gap would fix a real reliability problem for everyone running multi-agent OpenClaw on Discord, without changing any public config surface.

Happy to test patches and provide additional repro detail if helpful.

Steps to reproduce

  1. Set up two Discord bot accounts under channels.discord.accounts (e.g. alchemist, sentinel), both sharing a guild/channel.
  2. Configure outbound aliases:
    {
      "channels": {
        "discord": {
          "mentionAliases": {
            "Sentinel": "1485891428809707651"
          },
          "accounts": { "alchemist": { ... }, "sentinel": { ... } }
        }
      }
    }
  3. Have alchemist produce a natural reply that contains @Sentinel ping test (i.e. let the LLM emit this in its final text, not via a message tool call).
  4. Observe the message arriving in Discord.

Expected behavior

@Sentinel is rewritten to <@1485891428809707651> before the outbound send, so Sentinel receives a real Discord ping.

Actual behavior

The literal string @Sentinel ping test reaches Discord. It renders as plain text — no mention, no notification, no entry in message.mentions. The session reply text never appears in any plugin hook log either (message_sending, before_dispatch, reply_dispatch are all silent for this code path).

In contrast, if the same message is sent via the message tool (message({channel:"discord", to:"channel:<id>", message:"@Sentinel ping test"})), it is correctly rewritten to <@1485891428809707651> and pings.

OpenClaw version

  • OpenClaw version: 2026.5.12

Operating system

mac os

Install method

No response

Model

glm5.1

Provider / routing chain

openclaw -> glm5.1 ->openclaw -> discord

Additional provider/model setup details

No response

Logs, screenshots, and evidence

Impact and severity

No response

Additional information

No response

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

@Sentinel is rewritten to <@1485891428809707651> before the outbound send, so Sentinel receives a real Discord ping.

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]: Discord mentionAliases is not applied to session reply / final assistant text, only to message tool calls