diff --git a/package.json b/package.json index f26b60a..03a1e05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ode", - "version": "0.1.16", + "version": "0.1.17", "description": "Coding anywhere with your coding agents connected", "module": "packages/core/index.ts", "type": "module", diff --git a/packages/core/kernel/runtime-facade.ts b/packages/core/kernel/runtime-facade.ts index b770929..efa0c8b 100644 --- a/packages/core/kernel/runtime-facade.ts +++ b/packages/core/kernel/runtime-facade.ts @@ -84,8 +84,8 @@ export class KernelRuntimeFacade { evaluate: (event) => defaultInboundPolicy({ selfMessage: event.selfMessage, threadOwnerMessage: event.threadOwnerMessage, - threadParticipantBotCount: event.threadParticipantBotCount, isTopLevel: event.isTopLevel, + hasAnyMention: event.hasAnyMention ?? event.mentionedBot, mentionedBot: event.mentionedBot, activeThread: event.activeThread, normalizedText: event.normalizedText, @@ -104,8 +104,8 @@ export class KernelRuntimeFacade { const decision = defaultInboundPolicy({ selfMessage: event.selfMessage, threadOwnerMessage: event.threadOwnerMessage, - threadParticipantBotCount: event.threadParticipantBotCount, isTopLevel: event.isTopLevel, + hasAnyMention: event.hasAnyMention ?? event.mentionedBot, mentionedBot: event.mentionedBot, activeThread: event.activeThread, normalizedText: event.normalizedText, @@ -163,8 +163,8 @@ export class KernelRuntimeFacade { userId, selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: false, + hasAnyMention: false, mentionedBot: true, activeThread: true, rawText: selection, @@ -267,8 +267,8 @@ export class KernelRuntimeFacade { userId: context.userId, selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: false, + hasAnyMention: false, mentionedBot: true, activeThread: true, rawText: text, diff --git a/packages/core/model/raw-inbound-event.ts b/packages/core/model/raw-inbound-event.ts index 0d2ff12..0fe95be 100644 --- a/packages/core/model/raw-inbound-event.ts +++ b/packages/core/model/raw-inbound-event.ts @@ -11,8 +11,8 @@ export type RawInboundEvent = Readonly<{ userId: string; selfMessage: boolean; threadOwnerMessage: boolean; - threadParticipantBotCount: number; isTopLevel: boolean; + hasAnyMention?: boolean; mentionedBot: boolean; activeThread: boolean; rawText: string; diff --git a/packages/core/test/runtime-e2e.test.ts b/packages/core/test/runtime-e2e.test.ts index 9f39035..7bc9959 100644 --- a/packages/core/test/runtime-e2e.test.ts +++ b/packages/core/test/runtime-e2e.test.ts @@ -73,7 +73,6 @@ function toInboundEvent(params: { userId: params.userId, selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: params.isTopLevel ?? false, mentionedBot: params.mentionedBot ?? true, activeThread: params.activeThread ?? true, @@ -319,7 +318,6 @@ describe("core runtime e2e", () => { userId: "UE2E-dis", selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: false, mentionedBot: true, activeThread: true, @@ -358,7 +356,6 @@ describe("core runtime e2e", () => { userId: "UE2E-lark", selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: true, mentionedBot: false, activeThread: false, @@ -378,7 +375,6 @@ describe("core runtime e2e", () => { userId: "UE2E-lark", selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: false, mentionedBot: true, activeThread: true, diff --git a/packages/core/test/runtime-resilience-e2e.test.ts b/packages/core/test/runtime-resilience-e2e.test.ts index b1f0df0..6704f26 100644 --- a/packages/core/test/runtime-resilience-e2e.test.ts +++ b/packages/core/test/runtime-resilience-e2e.test.ts @@ -146,7 +146,6 @@ function toInboundEvent(params: { userId: params.userId, selfMessage: false, threadOwnerMessage: true, - threadParticipantBotCount: 1, isTopLevel: false, mentionedBot: true, activeThread: true, diff --git a/packages/ims/discord/client.ts b/packages/ims/discord/client.ts index d5330e0..11f3e66 100644 --- a/packages/ims/discord/client.ts +++ b/packages/ims/discord/client.ts @@ -15,7 +15,6 @@ import { } from "@/config"; import { findReplyThreadIdByStatusMessageTs } from "@/config/local/sessions"; import { - getThreadParticipantBotIds, isThreadActive, loadSession, markThreadActive, @@ -464,6 +463,8 @@ async function startDiscordRuntimeInternal(reason: string): Promise { const threadId = message.channel.id; const text = message.content.trim(); + const mentioned = isBotMentioned(message, client.user.id); + const hasAnyMention = (message?.mentions?.users?.size ?? 0) > 0; if (await maybeHandleLauncherCommand({ text, message, @@ -472,7 +473,6 @@ async function startDiscordRuntimeInternal(reason: string): Promise { })) { return; } - const mentioned = isBotMentioned(message, client.user.id); const active = isThreadActive(parentId, threadId, processorId); const normalizedText = mentioned ? cleanBotMention(text, client.user.id) : text; const threadSession = loadSession(parentId, threadId); @@ -487,8 +487,8 @@ async function startDiscordRuntimeInternal(reason: string): Promise { userId: message.author.id, selfMessage: false, threadOwnerMessage: threadSession?.threadOwnerUserId === message.author.id, - threadParticipantBotCount: getThreadParticipantBotIds(parentId, threadId).length, isTopLevel: false, + hasAnyMention, mentionedBot: mentioned, activeThread: active, rawText: text, @@ -554,14 +554,14 @@ async function startDiscordRuntimeInternal(reason: string): Promise { rawChannelId: parentId, threadId: thread.id, replyThreadId: thread.id, - messageId: message.id, - userId: message.author.id, - selfMessage: false, - threadOwnerMessage: true, - threadParticipantBotCount: getThreadParticipantBotIds(parentId, thread.id).length, - isTopLevel: false, - mentionedBot: true, - activeThread: false, + messageId: message.id, + userId: message.author.id, + selfMessage: false, + threadOwnerMessage: true, + isTopLevel: false, + hasAnyMention: true, + mentionedBot: true, + activeThread: false, rawText: message.content, normalizedText: topLevelText, receivedAtMs: Date.now(), diff --git a/packages/ims/lark/client.ts b/packages/ims/lark/client.ts index bba96fa..8dec6ca 100644 --- a/packages/ims/lark/client.ts +++ b/packages/ims/lark/client.ts @@ -20,7 +20,6 @@ import { } from "@/config"; import { findReplyThreadIdByStatusMessageTs } from "@/config/local/sessions"; import { - getThreadParticipantBotIds, isThreadActive, loadSession, markThreadActive, @@ -1086,6 +1085,7 @@ async function processLarkIncomingEvent(event: LarkIncomingEvent, processorAppId const isMentioned = botOpenId ? (mentions.includes(botOpenId) || isBotMentionedInText(rawText, botOpenId)) : false; + const hasAnyMention = mentions.length > 0; const active = isThreadActive(channelId, threadId, processorId); const threadSession = loadSession(channelId, threadId); const text = stripLarkMentionMarkup(rawText); @@ -1100,8 +1100,8 @@ async function processLarkIncomingEvent(event: LarkIncomingEvent, processorAppId userId: senderOpenId, selfMessage: isSelfMessage, threadOwnerMessage: threadSession?.threadOwnerUserId === senderOpenId, - threadParticipantBotCount: getThreadParticipantBotIds(channelId, threadId).length, isTopLevel: topLevelMessage, + hasAnyMention, mentionedBot: isMentioned, activeThread: active, rawText, diff --git a/packages/ims/shared/inbound-policy.test.ts b/packages/ims/shared/inbound-policy.test.ts new file mode 100644 index 0000000..95e3815 --- /dev/null +++ b/packages/ims/shared/inbound-policy.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from "bun:test"; +import { defaultInboundPolicy } from "./inbound-policy"; + +describe("defaultInboundPolicy", () => { + it("drops thread messages that mention another target", () => { + const decision = defaultInboundPolicy({ + selfMessage: false, + threadOwnerMessage: true, + isTopLevel: false, + hasAnyMention: true, + mentionedBot: false, + activeThread: true, + normalizedText: "<@other> handle this", + }); + + expect(decision).toEqual({ kind: "ignore", reason: "not_mentioned_and_inactive" }); + }); + + it("keeps active-thread owner follow-ups without mentions", () => { + const decision = defaultInboundPolicy({ + selfMessage: false, + threadOwnerMessage: true, + isTopLevel: false, + hasAnyMention: false, + mentionedBot: false, + activeThread: true, + normalizedText: "continue", + }); + + expect(decision).toEqual({ kind: "message", text: "continue" }); + }); +}); diff --git a/packages/ims/shared/inbound-policy.ts b/packages/ims/shared/inbound-policy.ts index ed0c614..846a2f2 100644 --- a/packages/ims/shared/inbound-policy.ts +++ b/packages/ims/shared/inbound-policy.ts @@ -3,8 +3,8 @@ import type { InboundDecision } from "@/core/model/inbound-decision"; export function defaultInboundPolicy(params: { selfMessage: boolean; threadOwnerMessage: boolean; - threadParticipantBotCount: number; isTopLevel: boolean; + hasAnyMention: boolean; mentionedBot: boolean; activeThread: boolean; normalizedText: string; @@ -14,6 +14,10 @@ export function defaultInboundPolicy(params: { return { kind: "ignore", reason: "self_message" }; } + if (!params.isTopLevel && params.hasAnyMention && !params.mentionedBot) { + return { kind: "ignore", reason: "not_mentioned_and_inactive" }; + } + if (!params.isTopLevel && !params.mentionedBot) { if (!params.activeThread) { return { kind: "ignore", reason: "not_mentioned_and_inactive" }; diff --git a/packages/ims/slack/client.ts b/packages/ims/slack/client.ts index 7eea353..4027580 100644 --- a/packages/ims/slack/client.ts +++ b/packages/ims/slack/client.ts @@ -9,7 +9,6 @@ import { } from "@/config"; import { markdownToSlack, splitForSlack, truncateForSlack } from "./formatter"; import { - getThreadParticipantBotIds, isThreadActive, loadSession, } from "@/config/local/sessions"; @@ -482,7 +481,6 @@ export function setupMessageHandlers(): void { const session = loadSession(channelId, threadId); return session?.threadOwnerUserId === userId; }, - getThreadParticipantBotCount: (channelId, threadId) => getThreadParticipantBotIds(channelId, threadId).length, isThreadActive, postGeneralSettingsLauncher: postSlackGeneralSettingsLauncher, describeSettingsIssues: describeSlackSettingsIssues, diff --git a/packages/ims/slack/message-router.test.ts b/packages/ims/slack/message-router.test.ts index b100566..e44b04e 100644 --- a/packages/ims/slack/message-router.test.ts +++ b/packages/ims/slack/message-router.test.ts @@ -15,7 +15,6 @@ function createDeps(overrides: Partial {}, setChannelWorkspaceAuth: () => {}, isThreadOwner: () => false, - getThreadParticipantBotCount: () => 1, isThreadActive: () => false, postGeneralSettingsLauncher: async () => {}, describeSettingsIssues: () => [], @@ -243,7 +242,6 @@ describe("registerSlackMessageRouter", () => { }, isThreadActive: () => true, isThreadOwner: () => true, - getThreadParticipantBotCount: () => 2, handleInboundEvent, }); diff --git a/packages/ims/slack/message-router.ts b/packages/ims/slack/message-router.ts index 11fd68a..2278bee 100644 --- a/packages/ims/slack/message-router.ts +++ b/packages/ims/slack/message-router.ts @@ -26,7 +26,6 @@ type RouterDeps = { auth: { workspaceId?: string; workspaceName?: string; botToken?: string; [key: string]: unknown } | undefined ) => void; isThreadOwner: (channelId: string, threadId: string, userId: string) => boolean; - getThreadParticipantBotCount: (channelId: string, threadId: string) => number; isThreadActive: (channelId: string, threadId: string, botId: string) => boolean; postGeneralSettingsLauncher: (channelId: string, userId: string, client: any) => Promise; describeSettingsIssues: (channelId: string) => string[]; @@ -118,15 +117,6 @@ function extractIncomingMessageData(message: any): IncomingMessageData | null { }; } -function shouldDropForOtherMentions(text: string, isMention: boolean): boolean { - return extractMentionedUserIds(text).length > 0 && !isMention; -} - -function tokenLast6(token?: string): string | undefined { - if (!token) return undefined; - return token.slice(-6); -} - async function maybeRefreshWorkspaceForMention(params: { deps: RouterDeps; channelId: string; @@ -275,6 +265,7 @@ export function registerSlackMessageRouter(deps: RouterDeps): void { || (Boolean(identity.botId) && (messageBotId === identity.botId || messageBotProfileId === identity.botId)); const mentionedUserIds = extractMentionedUserIds(text); + const hasAnyMention = mentionedUserIds.length > 0; const isMention = currentBotUserId ? mentionedUserIds.includes(currentBotUserId) : false; const cleanText = stripBotMention(text, currentBotUserId); logSlackTrace("Slack mention parse", { @@ -298,7 +289,6 @@ export function registerSlackMessageRouter(deps: RouterDeps): void { const runtimeBotId = contextBotToken ?? workspaceAuth?.botToken ?? "default"; const isTopLevel = threadId === messageId; const threadOwnerMessage = deps.isThreadOwner(channelId, threadId, userId); - const threadParticipantBotCount = deps.getThreadParticipantBotCount(channelId, threadId); const threadActive = deps.isThreadActive(channelId, threadId, runtimeBotId); const inboundEvent: RawInboundEvent = { platform: "slack", @@ -311,8 +301,8 @@ export function registerSlackMessageRouter(deps: RouterDeps): void { userId, selfMessage, threadOwnerMessage, - threadParticipantBotCount, isTopLevel, + hasAnyMention, mentionedBot: isMention, activeThread: threadActive, rawText: text, @@ -328,7 +318,6 @@ export function registerSlackMessageRouter(deps: RouterDeps): void { mentionedUserIds, isMention, threadOwnerMessage, - threadParticipantBotCount, threadActive, flowType: flowResult.type, flowReason: flowResult.type === "ignore" ? flowResult.reason : undefined, @@ -347,20 +336,6 @@ export function registerSlackMessageRouter(deps: RouterDeps): void { }); } - if (shouldDropForOtherMentions(text, isMention)) { - log.info("[DROP] Mentions other user", { - channelId, - threadId, - messageId, - imName: workspaceAuth?.workspaceName ?? deps.getChannelWorkspaceName(channelId) ?? "unknown", - botTokenLast6: tokenLast6(workspaceAuth?.botToken), - botUserId: currentBotUserId || "unknown", - mentionedUserIds, - isMention, - }); - return; - } - if (await maybeHandleLauncherCommand({ deps, cleanText, diff --git a/packages/ims/slack/slack-inbound-adapter.ts b/packages/ims/slack/slack-inbound-adapter.ts index 25ae8cc..eb3cebd 100644 --- a/packages/ims/slack/slack-inbound-adapter.ts +++ b/packages/ims/slack/slack-inbound-adapter.ts @@ -8,8 +8,8 @@ export class SlackInboundAdapter implements InboundAdapter { return defaultInboundPolicy({ selfMessage: event.selfMessage, threadOwnerMessage: event.threadOwnerMessage, - threadParticipantBotCount: event.threadParticipantBotCount, isTopLevel: event.isTopLevel, + hasAnyMention: event.hasAnyMention ?? event.mentionedBot, mentionedBot: event.mentionedBot, activeThread: event.activeThread, normalizedText: event.normalizedText,