fix(router): trust SDK isMention signal; drop broken hasMention regex
The router's mention / mention-sticky engage check was regex-matching @<agent_group.name> (e.g. @Andy) against message text. Platforms don't work that way — users address bots via the bot's platform username (@nanoclaw_v2_refactr_1_bot on Telegram, user-id mentions on Slack / Discord). The regex matched only coincidentally and never on Telegram, so mention-mode wirings silently never fired there. Two parallel mention detectors existed: the Chat SDK's onNewMention, which correctly resolves the bot's platform identity, and the router's hasMention text regex, which ignored the SDK verdict and invented its own heuristic. The router's detector was wrong in principle — the agent group's display name is a NanoClaw-side nickname, not a platform address. Thread the SDK signal through: InboundMessage gains an optional `isMention` field, the bridge sets it from each handler (onNewMention → true, onDirectMessage → true, onSubscribedMessage → message.isMention, onNewMessage(/./) → false), src/index.ts forwards it into InboundEvent, and evaluateEngage now checks `isMention === true` for mention modes. hasMention deleted entirely — there is only one source of truth for "did the user mention this bot": the platform / SDK. Agent-name-in-text matching for disambiguating multiple agents wired to one chat is a separate feature; users can express it today with engage_mode='pattern' + the agent's name as the regex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,15 @@ export interface InboundEvent {
|
||||
kind: 'chat' | 'chat-sdk';
|
||||
content: string; // JSON blob
|
||||
timestamp: string;
|
||||
/**
|
||||
* Platform-confirmed bot-mention signal forwarded from the adapter.
|
||||
* When defined, it's authoritative — use this instead of text-matching
|
||||
* agent_group_name, which breaks on platforms where the mention token
|
||||
* is the bot's platform username (e.g. Telegram). undefined means the
|
||||
* adapter doesn't provide the signal; evaluateEngage falls back to
|
||||
* agent-name regex.
|
||||
*/
|
||||
isMention?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,6 +203,7 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
// engage later. Drop policy = skip silently.
|
||||
const parsed = safeParseContent(event.message.content);
|
||||
const messageText = parsed.text ?? '';
|
||||
const isMention = event.message.isMention === true;
|
||||
|
||||
let engagedCount = 0;
|
||||
let accumulatedCount = 0;
|
||||
@@ -202,7 +212,7 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
const agentGroup = getAgentGroup(agent.agent_group_id);
|
||||
if (!agentGroup) continue;
|
||||
|
||||
const engages = evaluateEngage(agent, agentGroup, messageText, mg, event.threadId);
|
||||
const engages = evaluateEngage(agent, messageText, isMention, mg, event.threadId);
|
||||
|
||||
const accessOk = engages && (!accessGate || accessGate(event, userId, mg, agent.agent_group_id).allowed);
|
||||
const scopeOk = engages && (!senderScopeGate || senderScopeGate(event, userId, mg, agent).allowed);
|
||||
@@ -241,17 +251,26 @@ export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
* Decide whether a given wired agent should engage on this message.
|
||||
*
|
||||
* 'pattern' — regex test on text; '.' = always
|
||||
* 'mention' — bot must be @-mentioned by its agent-group name
|
||||
* 'mention-sticky' — @mention OR an active per-thread session already
|
||||
* exists for this (agent, mg, thread). The session
|
||||
* existence IS our subscription state; once a thread
|
||||
* has engaged us once, follow-ups arrive with no
|
||||
* mention and should still fire.
|
||||
* 'mention' — bot must be mentioned on the platform. Resolved by
|
||||
* the adapter (SDK-level) and forwarded as
|
||||
* `event.message.isMention`. Agent display name
|
||||
* (`agent_group.name`) is irrelevant — users address
|
||||
* the bot via its platform username (@botname on
|
||||
* Telegram, user-id mention on Slack/Discord), not
|
||||
* via the agent's NanoClaw-side display name. If a
|
||||
* user wants to disambiguate between multiple agents
|
||||
* wired to one chat, use engage_mode='pattern' with
|
||||
* the disambiguator as the regex.
|
||||
* 'mention-sticky' — platform mention OR an active per-thread session
|
||||
* already exists for this (agent, mg, thread). The
|
||||
* session existence IS our subscription state; once
|
||||
* a thread has engaged us once, follow-ups arrive
|
||||
* with no mention and should still fire.
|
||||
*/
|
||||
function evaluateEngage(
|
||||
agent: MessagingGroupAgent,
|
||||
agentGroup: AgentGroup,
|
||||
text: string,
|
||||
isMention: boolean,
|
||||
mg: MessagingGroup,
|
||||
threadId: string | null,
|
||||
): boolean {
|
||||
@@ -267,9 +286,9 @@ function evaluateEngage(
|
||||
}
|
||||
}
|
||||
case 'mention':
|
||||
return hasMention(text, agentGroup.name);
|
||||
return isMention;
|
||||
case 'mention-sticky': {
|
||||
if (hasMention(text, agentGroup.name)) return true;
|
||||
if (isMention) return true;
|
||||
// Sticky follow-up: session already exists for this (agent, mg, thread)
|
||||
// — the thread was activated before, keep firing.
|
||||
if (mg.is_group === 0) return false; // DMs never use mention-sticky sensibly
|
||||
@@ -281,12 +300,6 @@ function evaluateEngage(
|
||||
}
|
||||
}
|
||||
|
||||
function hasMention(text: string, agentName: string): boolean {
|
||||
if (!agentName) return false;
|
||||
const escaped = agentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
return new RegExp(`@${escaped}\\b`, 'i').test(text);
|
||||
}
|
||||
|
||||
async function deliverToAgent(
|
||||
agent: MessagingGroupAgent,
|
||||
agentGroup: AgentGroup,
|
||||
|
||||
Reference in New Issue
Block a user