Skip to content

[Bug]: Discord DM pairing identity mismatch breaks PluralKit users; extractDiscordSessionKind regex missing "direct" peer kind #86332

@r0c1n4nte

Description

@r0c1n4nte

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-security-reviewClawSweeper marked this issue as needing security-sensitive review.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:securitySecurity boundary, credential, authz, sandbox, or sensitive-data risk.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions