openclaw - 💡(How to fix) Fix [Bug]: Discord DM pairing identity mismatch breaks PluralKit users; extractDiscordSessionKind regex missing "direct" peer kind [1 pull requests]

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…

Two DM-handling bugs in the Discord plugin:

  1. PluralKit DM pairing infinite loop. When PluralKit transforms the sender identity, the DM preflight passes the raw gateway author to handleDiscordDmCommandDecision (which stores the pairing key) but the access check above it uses the resolved sender identity from resolveDiscordSenderIdentity. These two identities differ for PK users: the pairing is stored under a Discord user/webhook ID while the ingress resolver looks it up under the PK member UUID. Every inbound DM triggers a fresh pairing challenge — the previously-completed one is never found.

  2. extractDiscordSessionKind regex doesn't match the direct peer kind. The regex /discord:(channel|group|dm):/ in conversation-identity.ts doesn't match discord:direct:, which is the canonical peer kind used for DM session keys throughout the plugin. Works by coincidence today because downstream === "dm" checks treat the resulting null the same as an unrecognised kind, but a latent fragility.


Error Message

Logic error (behaviour inconsistent between two code paths in the same function)

Root Cause

  1. extractDiscordSessionKind regex doesn't match the direct peer kind. The regex /discord:(channel|group|dm):/ in conversation-identity.ts doesn't match discord:direct:, which is the canonical peer kind used for DM session keys throughout the plugin. Works by coincidence today because downstream === "dm" checks treat the resulting null the same as an unrecognised kind, but a latent fragility.

Fix Action

Fixed

Code Example

// line ~55 — access check uses the resolved PK-aware identity
const dmAccess = await resolveDiscordDmCommandAccess({
  sender: {
    id: params.sender.id,      // PK member UUID
    name: params.sender.name,
    tag: params.sender.tag
  },
  ...
});

// line ~69 — pairing handler receives the raw gateway author instead
await handleDiscordDmCommandDecision({
  senderAccess: dmAccess.senderAccess,
  sender: {
    id: params.author.id,      // ← Discord user/webhook ID (should be params.sender)
    tag: formatDiscordUserTag(params.author),
    name: params.author.username ?? void 0
  },
  onPairingCreated: async (code) => {
    await sendMessageDiscord(`user:${params.author.id}`, ...);
  },
});

---

function extractDiscordSessionKind(sessionKey) {
  if (!sessionKey) return null;
  const match = sessionKey.match(/discord:(channel|group|dm):/);
  //                                               ^^^^^^^^^^^^^^^^
  //                                               missing |direct
  if (!match) return null;
  return match[1];
}
RAW_BUFFERClick to expand / collapse

Bug type

Logic error (behaviour inconsistent between two code paths in the same function)

Summary

Two DM-handling bugs in the Discord plugin:

  1. PluralKit DM pairing infinite loop. When PluralKit transforms the sender identity, the DM preflight passes the raw gateway author to handleDiscordDmCommandDecision (which stores the pairing key) but the access check above it uses the resolved sender identity from resolveDiscordSenderIdentity. These two identities differ for PK users: the pairing is stored under a Discord user/webhook ID while the ingress resolver looks it up under the PK member UUID. Every inbound DM triggers a fresh pairing challenge — the previously-completed one is never found.

  2. extractDiscordSessionKind regex doesn't match the direct peer kind. The regex /discord:(channel|group|dm):/ in conversation-identity.ts doesn't match discord:direct:, which is the canonical peer kind used for DM session keys throughout the plugin. Works by coincidence today because downstream === "dm" checks treat the resulting null the same as an unrecognised kind, but a latent fragility.


Environment

  • OpenClaw version: 2026.5.22 (installed via npm @openclaw/discord)
  • Files affected:
    • extensions/discord/src/monitor/message-handler.preflight.tsresolveDiscordDmPreflightAccess
    • extensions/discord/src/conversation-identity.tsextractDiscordSessionKind
  • Requires PluralKit: Bug 1 yes, Bug 2 no

Steps to Reproduce (Bug 1)

  1. Configure Discord with dmPolicy: "pairing" and PluralKit integration enabled
  2. Have a PluralKit-proxied user send a DM to the bot
  3. Bot issues a pairing challenge (expected)
  4. Complete the pairing via /pair <code>
  5. Same PK user sends another DM
  6. Bug: bot issues a fresh pairing challenge instead of recognising the paired user

Steps to Reproduce (Bug 2)

Not user-visible today — verify by inspecting extractDiscordSessionKind("agent:agentId:discord:direct:123456") returns null instead of "direct".


Root Cause (Bug 1)

In resolveDiscordDmPreflightAccess:

// line ~55 — access check uses the resolved PK-aware identity
const dmAccess = await resolveDiscordDmCommandAccess({
  sender: {
    id: params.sender.id,      // PK member UUID
    name: params.sender.name,
    tag: params.sender.tag
  },
  ...
});

// line ~69 — pairing handler receives the raw gateway author instead
await handleDiscordDmCommandDecision({
  senderAccess: dmAccess.senderAccess,
  sender: {
    id: params.author.id,      // ← Discord user/webhook ID (should be params.sender)
    tag: formatDiscordUserTag(params.author),
    name: params.author.username ?? void 0
  },
  onPairingCreated: async (code) => {
    await sendMessageDiscord(`user:${params.author.id}`, ...);
  },
});

resolveDiscordDmCommandAccess resolves the ingress subject via createDiscordDmIngressSubject(params.sender), which sets stableId = sender.id (the PK member UUID). The ingress resolver stores pairing records keyed by that stableId. But handleDiscordDmCommandDecision creates the pairing record using params.author.id (the Discord user/webhook ID). On the next inbound message the ingress resolver looks up the PK member UUID and finds nothing.

When PluralKit is not active params.sender.id === params.author.id, so the bug is invisible.

Root Cause (Bug 2)

function extractDiscordSessionKind(sessionKey) {
  if (!sessionKey) return null;
  const match = sessionKey.match(/discord:(channel|group|dm):/);
  //                                               ^^^^^^^^^^^^^^^^
  //                                               missing |direct
  if (!match) return null;
  return match[1];
}

DM session keys use the direct peer kind (agent:<id>:discord:direct:<userId>), normalised from legacy dm in session-key-normalization.ts. The regex only matches channel, group, and dm. It doesn't match direct.

The three callers of extractDiscordSessionKind all check for === "dm" and treat null the same way (return null / skip), so there's no visible breakage today. The fix is still worth making to prevent a future code path from treating null differently.


Impact

  • Bug 1: PluralKit users cannot use DMs with dmPolicy: "pairing". Every message triggers a new challenge. Allowlist users may also be affected if PK transforms the identity before the ingress check.
  • Bug 2: No user-visible impact today. Latent risk if approval-native resolution ever adds a null-specific branch.

Related

  • PR #55413 ("canonicalize PluralKit inbound ids") — touched the same preflight area for message-ID dedup, closed without merging
  • PR #70944 ("resolve trusted principals via identity links") — open, touches sender identity resolution in the same files but doesn't address this mismatch
  • Issue #48641 ("Discord DMs: inbound messages silently dropped") — different root cause (allowFrom merge semantics) but similar symptoms

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