From 4ed2ea5035b454c21b6ce29c7b0f7b7ea4c37677 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 22 Apr 2026 12:36:11 -0700 Subject: [PATCH] fix(hooks): tighten thread ownership mention matching --- extensions/thread-ownership/index.test.ts | 44 +++++++++++++++++++++++ extensions/thread-ownership/index.ts | 15 ++++++-- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/extensions/thread-ownership/index.test.ts b/extensions/thread-ownership/index.test.ts index 6fcea2897a6..e82b28c202a 100644 --- a/extensions/thread-ownership/index.test.ts +++ b/extensions/thread-ownership/index.test.ts @@ -325,5 +325,49 @@ describe("thread-ownership plugin", () => { expect(result).toBeUndefined(); expect(globalThis.fetch).not.toHaveBeenCalled(); }); + + it("does not treat superset handles as agent-name mentions", async () => { + await hooks.message_received( + { + content: "hey @testbot2 help", + threadId: "8888.0003", + metadata: { channelId: "C789" }, + }, + { channelId: "slack", conversationId: "C789" }, + ); + + vi.mocked(globalThis.fetch).mockResolvedValue( + new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), + ); + + await hooks.message_sending( + { content: "On it!", replyToId: "8888.0003", metadata: { channelId: "C789" }, to: "C789" }, + { channelId: "slack", conversationId: "C789" }, + ); + + expect(globalThis.fetch).toHaveBeenCalled(); + }); + + it("does not treat email-like text as an agent-name mention", async () => { + await hooks.message_received( + { + content: "send mail to foo@testbot.com", + threadId: "8888.0004", + metadata: { channelId: "C789" }, + }, + { channelId: "slack", conversationId: "C789" }, + ); + + vi.mocked(globalThis.fetch).mockResolvedValue( + new Response(JSON.stringify({ owner: "test-agent" }), { status: 200 }), + ); + + await hooks.message_sending( + { content: "On it!", replyToId: "8888.0004", metadata: { channelId: "C789" }, to: "C789" }, + { channelId: "slack", conversationId: "C789" }, + ); + + expect(globalThis.fetch).toHaveBeenCalled(); + }); }); }); diff --git a/extensions/thread-ownership/index.ts b/extensions/thread-ownership/index.ts index 79df6baea13..c65f7551bf2 100644 --- a/extensions/thread-ownership/index.ts +++ b/extensions/thread-ownership/index.ts @@ -24,6 +24,10 @@ function resolveThreadToken(value: unknown): string { return typeof value === "string" || typeof value === "number" ? String(value) : ""; } +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + function resolveSlackConversationId(value: unknown): string { const raw = normalizeOptionalString(value) ?? ""; if (!raw) { @@ -44,6 +48,14 @@ function cleanExpiredMentions(): void { } } +function containsAgentNameMention(text: string, agentName: string): boolean { + const trimmedName = agentName.trim(); + if (!trimmedName) { + return false; + } + return new RegExp(`(^|[^\\w])@${escapeRegExp(trimmedName)}(?=$|[^\\w])`, "i").test(text); +} + function resolveOwnershipAgent(config: OpenClawConfig): { id: string; name: string } { const list = Array.isArray(config.agents?.list) ? config.agents.list.filter( @@ -91,7 +103,6 @@ export default definePluginEntry({ } const text = event.content ?? ""; - const normalizedText = text.toLowerCase(); const threadTs = resolveThreadToken(event.threadId) || resolveThreadToken(event.metadata?.threadId) || @@ -105,7 +116,7 @@ export default definePluginEntry({ } const mentioned = - (agentName && normalizedText.includes(`@${agentName.toLowerCase()}`)) || + containsAgentNameMention(text, agentName) || (botUserId && text.includes(`<@${botUserId}>`)); if (mentioned) { cleanExpiredMentions();