fix(discord): suppress bound thread webhook copies

This commit is contained in:
Peter Steinberger
2026-05-02 05:29:33 +01:00
parent 66d8fcea99
commit bca4e440bb
3 changed files with 62 additions and 15 deletions

View File

@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/logging: keep deferred channel startup logs on the subsystem logger, so Slack, Discord, Telegram, and voice-call startup messages keep timestamped prefixes. Thanks @vincentkoc.
- Discord/threads: ignore webhook-authored copies in already-bound Discord session threads even when the webhook id differs, preventing PluralKit proxy copies from creating duplicate turn pressure. Fixes #52005. Thanks @acgh213.
- Discord/threads: return the created thread as partial success when the follow-up initial message fails, so agents do not retry thread creation and create empty duplicate threads. Fixes #48450. Thanks @dahifi.
- Discord/components: consume every button or select in a non-reusable component message after the first authorized click, so single-use panels cannot fire sibling callbacks. Fixes #54227. Thanks @fujiwarakasei.
- Discord/reactions: skip reaction listener registration when DMs and group DMs are disabled and every configured guild has `reactionNotifications: "off"`, avoiding needless reaction-event queue work. Fixes #47516. Thanks @x4v13r1120.

View File

@@ -146,16 +146,19 @@ export function shouldIgnoreBoundThreadWebhookMessage(params: {
normalizeOptionalString(params.threadBinding?.webhookId) ??
normalizeOptionalString(params.threadBinding?.metadata?.webhookId) ??
"";
if (!boundWebhookId) {
const threadId = normalizeOptionalString(params.threadId) ?? "";
if (!threadId) {
return false;
}
return isRecentlyUnboundThreadWebhookMessage({
accountId: params.accountId,
threadId,
webhookId,
});
if (boundWebhookId && webhookId === boundWebhookId) {
return true;
}
return webhookId === boundWebhookId;
const threadId = normalizeOptionalString(params.threadId) ?? "";
if (!threadId) {
return false;
}
if (params.threadBinding) {
return true;
}
return isRecentlyUnboundThreadWebhookMessage({
accountId: params.accountId,
threadId,
webhookId,
});
}

View File

@@ -624,7 +624,7 @@ describe("preflightDiscordMessage", () => {
expect(result?.boundSessionKey).toBe(threadBinding.targetSessionKey);
});
it("drops hydrated bound-thread webhook echoes after fetching an empty payload", async () => {
it("drops hydrated bound-thread webhook copies after fetching an empty payload", async () => {
const threadBinding = createThreadBinding({
targetKind: "session",
targetSessionKey: "agent:main:acp:discord-thread-1",
@@ -685,6 +685,38 @@ describe("preflightDiscordMessage", () => {
expect(result).toBeNull();
});
it("drops bound-thread webhook copies from other webhook ids", async () => {
const threadBinding = createThreadBinding({
targetKind: "session",
targetSessionKey: "agent:main:acp:discord-thread-1",
});
const threadId = "thread-webhook-proxy-1";
const parentId = "channel-parent-webhook-proxy-1";
const message = createDiscordMessage({
id: "m-webhook-proxy-1",
channelId: threadId,
content: "proxied user message",
webhook_id: "pluralkit-webhook-1",
author: {
id: "relay-bot-1",
bot: true,
username: "Proxy",
},
});
const result = await runThreadBoundPreflight({
threadId,
parentId,
message,
threadBinding,
discordConfig: {
allowBots: true,
} as DiscordConfig,
});
expect(result).toBeNull();
});
it("bypasses mention gating in bound threads for allowed bot senders", async () => {
const threadBinding = createThreadBinding();
const threadId = "thread-bot-focus";
@@ -1445,18 +1477,20 @@ describe("shouldIgnoreBoundThreadWebhookMessage", () => {
).toBe(true);
});
it("returns false when webhook ids differ", () => {
it("returns true when a bound thread receives a different webhook id", () => {
expect(
shouldIgnoreBoundThreadWebhookMessage({
threadId: "thread-1",
webhookId: "wh-other",
threadBinding: createThreadBinding(),
}),
).toBe(false);
).toBe(true);
});
it("returns false when there is no bound thread webhook", () => {
it("returns true when a bound thread receives a webhook without a recorded bound webhook id", () => {
expect(
shouldIgnoreBoundThreadWebhookMessage({
threadId: "thread-1",
webhookId: "wh-1",
threadBinding: createThreadBinding({
metadata: {
@@ -1464,6 +1498,15 @@ describe("shouldIgnoreBoundThreadWebhookMessage", () => {
},
}),
}),
).toBe(true);
});
it("returns false for differing webhook ids without a known thread id", () => {
expect(
shouldIgnoreBoundThreadWebhookMessage({
webhookId: "wh-other",
threadBinding: createThreadBinding(),
}),
).toBe(false);
});