From 189cd99377bf3e5b699c109b8292d80a06edbaa6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 01:12:26 +0000 Subject: [PATCH] refactor(discord): require explicit outbound target hints --- src/infra/outbound/outbound-session.ts | 27 ++++++++++++++++++++-- src/infra/outbound/outbound.test.ts | 32 ++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/infra/outbound/outbound-session.ts b/src/infra/outbound/outbound-session.ts index 3655c6e69..0169e9c0b 100644 --- a/src/infra/outbound/outbound-session.ts +++ b/src/infra/outbound/outbound-session.ts @@ -4,7 +4,7 @@ import { getChannelPlugin } from "../../channels/plugins/index.js"; import type { ChannelId } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { recordSessionMetaFromInbound, resolveStorePath } from "../../config/sessions.js"; -import { parseDiscordTarget } from "../../discord/targets.js"; +import { parseDiscordTarget, type DiscordTargetKind } from "../../discord/targets.js"; import { parseIMessageTarget, normalizeIMessageHandle } from "../../imessage/targets.js"; import { buildAgentSessionKey, type RoutePeer } from "../../routing/resolve-route.js"; import { resolveThreadSessionKeys } from "../../routing/session-key.js"; @@ -239,7 +239,9 @@ async function resolveSlackSession( function resolveDiscordSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { - const parsed = parseDiscordTarget(params.target, { defaultKind: "channel" }); + const parsed = parseDiscordTarget(params.target, { + defaultKind: resolveDiscordOutboundTargetKindHint(params), + }); if (!parsed) { return null; } @@ -274,6 +276,27 @@ function resolveDiscordSession( }; } +function resolveDiscordOutboundTargetKindHint( + params: ResolveOutboundSessionRouteParams, +): DiscordTargetKind | undefined { + const resolvedKind = params.resolvedTarget?.kind; + if (resolvedKind === "user") { + return "user"; + } + if (resolvedKind === "group" || resolvedKind === "channel") { + return "channel"; + } + + const target = params.target.trim(); + if (/^channel:/i.test(target)) { + return "channel"; + } + if (/^(user:|discord:|@|<@!?)/i.test(target)) { + return "user"; + } + return undefined; +} + function resolveTelegramSession( params: ResolveOutboundSessionRouteParams, ): OutboundSessionRoute | null { diff --git a/src/infra/outbound/outbound.test.ts b/src/infra/outbound/outbound.test.ts index 960c30625..5cd7f78b8 100644 --- a/src/infra/outbound/outbound.test.ts +++ b/src/infra/outbound/outbound.test.ts @@ -1120,6 +1120,38 @@ describe("resolveOutboundSessionRoute", () => { } } }); + + it("uses resolved Discord user targets to route bare numeric ids as DMs", async () => { + const route = await resolveOutboundSessionRoute({ + cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, + channel: "discord", + agentId: "main", + target: "123", + resolvedTarget: { + to: "user:123", + kind: "user", + source: "directory", + }, + }); + + expect(route).toMatchObject({ + sessionKey: "agent:main:discord:direct:123", + from: "discord:123", + to: "user:123", + chatType: "direct", + }); + }); + + it("rejects bare numeric Discord targets when the caller has no kind hint", async () => { + await expect( + resolveOutboundSessionRoute({ + cfg: { session: { dmScope: "per-channel-peer" } } as OpenClawConfig, + channel: "discord", + agentId: "main", + target: "123", + }), + ).rejects.toThrow(/Ambiguous Discord recipient/); + }); }); describe("normalizeOutboundPayloadsForJson", () => {