fix(discord): normalize bare numeric IDs in outbound target resolution

Bare numeric Discord IDs (e.g. '1470130713209602050') in cron
delivery.to caused 'Ambiguous Discord recipient' errors and silent
delivery failures.

Adds normalizeDiscordOutboundTarget() to the existing Discord
normalize module (channels/plugins/normalize/discord.ts) alongside
normalizeDiscordMessagingTarget. Defaults bare numeric IDs to
'channel:<id>', matching existing behavior.

Both the Discord extension plugin and standalone outbound adapter
use the shared helper via a one-liner resolveTarget.

Fixes #14753. Related: #13927
This commit is contained in:
Latitude Bot
2026-02-13 07:09:49 -05:00
committed by Shadow
parent 250896cf6e
commit 3238bd78d9
5 changed files with 63 additions and 0 deletions

View File

@@ -16,6 +16,7 @@ import {
migrateBaseNameToDefaultAccount,
normalizeAccountId,
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
PAIRING_APPROVED_MESSAGE,
resolveDiscordAccount,
resolveDefaultDiscordAccountId,
@@ -291,6 +292,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
chunker: null,
textChunkLimit: 2000,
pollMaxOptions: 10,
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
sendText: async ({ to, text, accountId, deps, replyToId, silent }) => {
const send = deps?.sendDiscord ?? getDiscordRuntime().channel.discord.sendMessageDiscord;
const result = await send(to, text, {

View File

@@ -6,6 +6,29 @@ export function normalizeDiscordMessagingTarget(raw: string): string | undefined
return target?.normalized;
}
/**
* Normalize a Discord outbound target for delivery. Bare numeric IDs are
* prefixed with "channel:" to avoid the ambiguous-target error in
* parseDiscordTarget. All other formats pass through unchanged.
*/
export function normalizeDiscordOutboundTarget(
to?: string,
): { ok: true; to: string } | { ok: false; error: Error } {
const trimmed = to?.trim();
if (!trimmed) {
return {
ok: false,
error: new Error(
'Discord recipient is required. Use "channel:<id>" for channels or "user:<id>" for DMs.',
),
};
}
if (/^\d+$/.test(trimmed)) {
return { ok: true, to: `channel:${trimmed}` };
}
return { ok: true, to: trimmed };
}
export function looksLikeDiscordTargetId(raw: string): boolean {
const trimmed = raw.trim();
if (!trimmed) {

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from "vitest";
import { normalizeDiscordOutboundTarget } from "../normalize/discord.js";
describe("normalizeDiscordOutboundTarget", () => {
it("normalizes bare numeric IDs to channel: prefix", () => {
expect(normalizeDiscordOutboundTarget("1470130713209602050")).toEqual({
ok: true,
to: "channel:1470130713209602050",
});
});
it("passes through channel: prefixed targets", () => {
expect(normalizeDiscordOutboundTarget("channel:123")).toEqual({ ok: true, to: "channel:123" });
});
it("passes through user: prefixed targets", () => {
expect(normalizeDiscordOutboundTarget("user:123")).toEqual({ ok: true, to: "user:123" });
});
it("passes through channel name strings", () => {
expect(normalizeDiscordOutboundTarget("general")).toEqual({ ok: true, to: "general" });
});
it("returns error for empty target", () => {
expect(normalizeDiscordOutboundTarget("").ok).toBe(false);
});
it("returns error for undefined target", () => {
expect(normalizeDiscordOutboundTarget(undefined).ok).toBe(false);
});
it("trims whitespace", () => {
expect(normalizeDiscordOutboundTarget(" 123 ")).toEqual({ ok: true, to: "channel:123" });
});
});

View File

@@ -1,11 +1,13 @@
import type { ChannelOutboundAdapter } from "../types.js";
import { sendMessageDiscord, sendPollDiscord } from "../../../discord/send.js";
import { normalizeDiscordOutboundTarget } from "../normalize/discord.js";
export const discordOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
chunker: null,
textChunkLimit: 2000,
pollMaxOptions: 10,
resolveTarget: ({ to }) => normalizeDiscordOutboundTarget(to),
sendText: async ({ to, text, accountId, deps, replyToId, silent }) => {
const send = deps?.sendDiscord ?? sendMessageDiscord;
const result = await send(to, text, {

View File

@@ -328,6 +328,7 @@ export { discordOnboardingAdapter } from "../channels/plugins/onboarding/discord
export {
looksLikeDiscordTargetId,
normalizeDiscordMessagingTarget,
normalizeDiscordOutboundTarget,
} from "../channels/plugins/normalize/discord.js";
export { collectDiscordStatusIssues } from "../channels/plugins/status-issues/discord.js";