openclaw - ✅(Solved) Fix Discord DM inbound sets ctx.To to channel:<id> instead of user:<id> [1 pull requests, 1 participants]

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…
GitHub stats
openclaw/openclaw#68126Fetched 2026-04-18 05:53:48
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1referenced ×1

Root Cause

In resolveDiscordAutoThreadReplyPlan (threading-*.js), originalReplyTarget is unconditionally constructed as channel:<messageChannelId> regardless of whether the message is a DM:

// threading-rB6oooGZ.js:759
const originalReplyTarget = `channel:${params.threadChannel?.id ?? (messageChannelId || "unknown")}`;

This replyTarget flows into ctx.To via effectiveTo in the message handler:

// message-handler-C5NTj1qX.js:538
const effectiveTo = autoThreadContext?.To ?? replyTarget;

Since autoThreadContext is null for DMs (only created when isGuildMessage is true), effectiveTo falls through to replyTarget, which is channel:<DM_channel_id>.

Note: OriginatingTo and lastRouteTo are correctly patched via dmConversationTarget (lines 543-547, 553), but ctx.To itself is not.

Fix Action

Fix / Workaround

Note: OriginatingTo and lastRouteTo are correctly patched via dmConversationTarget (lines 543-547, 553), but ctx.To itself is not.

PR fix notes

PR #68144: fix(discord): set ctx.To to user: target for DM inbound so downstream builds direct session keys

Description (problem / solution / changelog)

Summary

Fixes #68126 — Discord DM inbound messages were landing with `ctx.To = channel:<dmChannelId>` instead of `user:<userId>`. Downstream mirror and delivery-recovery then built `agent:main:discord:channel:<id>` session keys for what are semantically direct conversations, and Discord's API returned `Unknown Channel` errors on recovery reads.

Same fix family as the closed-without-merge attempt in #47626 (brokemac79, 2026-04-06); the bug was re-reported today with a fresh stack trace.

Root cause

In `extensions/discord/src/monitor/message-handler.process.ts`, `effectiveTo` was computed before `dmConversationTarget` was resolved:

```ts const effectiveTo = autoThreadContext?.To ?? replyTarget; // ← replyTarget is always channel:<dmChannelId> // ... later ... const dmConversationTarget = isDirectMessage ? resolveDiscordConversationIdentity({ isDirectMessage, userId: author.id }) : undefined; const lastRouteTo = dmConversationTarget ?? effectiveTo; // ← correctly user:... for DMs ```

`OriginatingTo` and `lastRouteTo` are already patched via `dmConversationTarget`, so the DM correction logic was already in the file — `ctx.To` itself was just missing it.

Fix

Reorder so `dmConversationTarget` is computed before `effectiveTo`, then thread it into the fallback chain:

```ts const effectiveTo = autoThreadContext?.To ?? dmConversationTarget ?? replyTarget; ```

The outgoing `deliverTarget` is unchanged because Discord's API sends DMs via channel id — only the semantic `ctx.To` that downstream consumers read is corrected. `lastRouteTo` simplifies naturally under the new order (`dmConversationTarget ?? effectiveTo` is equivalent when effectiveTo is already `dmConversationTarget` on DMs).

Test plan

  • Added a regression case in `extensions/discord/src/monitor/message-handler.process.test.ts`:
    • `sets ctx.To to user target for DM inbound so downstream builds direct session keys`
  • Widened the existing `getLastDispatchCtx` helper to expose `To`, `From`, and `ChatType` so downstream tests can assert ctx shape.
  • The existing DM lastRoute test (`stores DM lastRoute with user target for direct-session continuity`) still passes — the `lastRouteTo` derivation is unchanged in outcome.
  • `NODE_OPTIONS=--max-old-space-size=8192 npx tsc --noEmit` — 247 baseline on main, 247 on branch (no new errors).
  • `pnpm exec oxlint` on both touched files — 0 warnings, 0 errors.

Local full vitest is blocked by pre-existing `test/non-isolated-runner.ts` drift (reproduces on `main`) — CI runs the new case normally.

Risk

  • `deliverTarget` is untouched, so the outbound Discord API call is identical to today's behavior.
  • `OriginatingTo` already uses `dmConversationTarget ?? replyTarget` (line 464), so every downstream ctx field now agrees on the DM-user semantic.
  • Guild/channel paths (`isDirectMessage === false`) are unaffected because `dmConversationTarget` is `undefined` and the fallback chain collapses to the prior `replyTarget`.

Changed files

  • extensions/discord/src/monitor/message-handler.process.test.ts (modified, +36/-2)
  • extensions/discord/src/monitor/message-handler.process.ts (modified, +9/-5)

Code Example

// threading-rB6oooGZ.js:759
const originalReplyTarget = `channel:${params.threadChannel?.id ?? (messageChannelId || "unknown")}`;

---

// message-handler-C5NTj1qX.js:538
const effectiveTo = autoThreadContext?.To ?? replyTarget;

---

// Before
const effectiveTo = autoThreadContext?.To ?? replyTarget;

// After
const effectiveTo = autoThreadContext?.To ?? (isDirectMessage ? `user:${author.id}` : replyTarget);
RAW_BUFFERClick to expand / collapse

Bug Description

Discord DM (direct message) inbound messages have ctx.To incorrectly set to channel:<DM_channel_id> instead of user:<userId>. This causes downstream components (mirror, delivery-recovery) to treat the DM as a channel conversation, ultimately resulting in Discord API Unknown Channel errors.

Observed Behavior

  • to = channel:1467396478723952780
  • mirror.sessionKey = agent:main:discord:channel:1467396478723952780
  • chat_type = direct (correct)

Expected Behavior

  • to = user:<userId>
  • mirror.sessionKey should reflect direct semantics (e.g. agent:main:discord:direct:<userId>)
  • chat_type = direct

Root Cause

In resolveDiscordAutoThreadReplyPlan (threading-*.js), originalReplyTarget is unconditionally constructed as channel:<messageChannelId> regardless of whether the message is a DM:

// threading-rB6oooGZ.js:759
const originalReplyTarget = `channel:${params.threadChannel?.id ?? (messageChannelId || "unknown")}`;

This replyTarget flows into ctx.To via effectiveTo in the message handler:

// message-handler-C5NTj1qX.js:538
const effectiveTo = autoThreadContext?.To ?? replyTarget;

Since autoThreadContext is null for DMs (only created when isGuildMessage is true), effectiveTo falls through to replyTarget, which is channel:<DM_channel_id>.

Note: OriginatingTo and lastRouteTo are correctly patched via dmConversationTarget (lines 543-547, 553), but ctx.To itself is not.

Call Chain

  1. resolveDiscordAutoThreadReplyPlanoriginalReplyTarget = "channel:<DM_channel_id>"
  2. resolveDiscordReplyDeliveryPlan → passes through originalReplyTarget without DM correction
  3. effectiveTo = autoThreadContext?.To ?? replyTargetautoThreadContext is null for DMs → effectiveTo = "channel:<DM_channel_id>"
  4. ctx.To = effectiveTo → polluted
  5. Mirror / recovery reads ctx.To → gets channel:<id> → builds wrong session key → Discord returns Unknown Channel

Suggested Fix

In message-handler around line 538, apply DM correction to effectiveTo:

// Before
const effectiveTo = autoThreadContext?.To ?? replyTarget;

// After
const effectiveTo = autoThreadContext?.To ?? (isDirectMessage ? `user:${author.id}` : replyTarget);

deliverTarget should remain channel:<DM_channel_id> since Discord API sends DMs via channel ID — only the semantic ctx.To needs correction.

What Has Been Ruled Out

  • delivery-recovery: only replays pending entries, does not transform user:channel:
  • resolveSessionDeliveryTarget: reads from lastTo which is correctly set via dmConversationTarget
  • normalizeChatType: only normalizes dm/direct → direct, does not change to channel
  • buildDiscordRoutePeer: correctly returns kind: "direct" when isDirectMessage is true
  • resolveGroupSessionKey: correctly returns null for DMs (ChatType is "direct", From has no :channel:)

Environment

  • OpenClaw version: installed at /usr/local/lib/node_modules/openclaw/
  • Affected files (compiled):
    • dist/extensions/discord/threading-rB6oooGZ.js (line 759)
    • dist/extensions/discord/message-handler-C5NTj1qX.js (lines 538, 561)

extent analysis

TL;DR

The most likely fix is to apply a DM correction to effectiveTo in the message-handler to set ctx.To to user:<userId> for direct messages.

Guidance

  • Verify that the issue is caused by the incorrect setting of ctx.To to channel:<DM_channel_id> instead of user:<userId> for direct messages.
  • Apply the suggested fix by modifying the message-handler around line 538 to use the corrected effectiveTo calculation: const effectiveTo = autoThreadContext?.To ?? (isDirectMessage ? user:${author.id} : replyTarget);.
  • Ensure that deliverTarget remains channel:<DM_channel_id> as it is used for sending DMs via the Discord API.
  • Test the fix by sending a direct message and verifying that ctx.To is set to user:<userId> and that downstream components (mirror, delivery-recovery) function correctly.

Example

The corrected code snippet for the message-handler would be:

const effectiveTo = autoThreadContext?.To ?? (isDirectMessage ? `user:${author.id}` : replyTarget);

This change ensures that ctx.To is set to user:<userId> for direct messages, while keeping deliverTarget as channel:<DM_channel_id>.

Notes

This fix assumes that the isDirectMessage variable is correctly set to true for direct messages. If this variable is not reliable, additional debugging may be necessary to determine the correct condition for applying the DM correction.

Recommendation

Apply the workaround by modifying the message-handler to use the corrected effectiveTo calculation, as this fix directly addresses the root cause of the issue and has been suggested in the issue description.

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

openclaw - ✅(Solved) Fix Discord DM inbound sets ctx.To to channel:<id> instead of user:<id> [1 pull requests, 1 participants]