(event: AppMentionEvent)
| 709 | * Handle @mention in a channel |
| 710 | */ |
| 711 | export async function handleAppMention(event: AppMentionEvent): Promise<void> { |
| 712 | if (!initialized || !claudeClient) { |
| 713 | logger.warn('Addie: Not initialized, ignoring mention'); |
| 714 | return; |
| 715 | } |
| 716 | |
| 717 | const startTime = Date.now(); |
| 718 | const interactionId = generateInteractionId(); |
| 719 | |
| 720 | // Check if user is an AAO admin (for admin-only tools access) |
| 721 | const isAAOAdmin = await isSlackUserAAOAdmin(event.user); |
| 722 | logger.debug({ userId: event.user, isAAOAdmin }, 'Addie: Checked admin status for mention'); |
| 723 | |
| 724 | // Strip bot mention |
| 725 | const rawText = botUserId ? stripBotMention(event.text, botUserId) : event.text; |
| 726 | |
| 727 | // Resolve user mentions to include names (e.g., <@U123> -> <@U123|John>) |
| 728 | const textWithResolvedMentions = await resolveSlackMentions(rawText, lookupSlackUserName); |
| 729 | |
| 730 | // Sanitize input |
| 731 | const inputValidation = sanitizeInput(textWithResolvedMentions); |
| 732 | |
| 733 | // Build per-request context for system prompt |
| 734 | // Skip goals in channel mentions to prevent membership pitching |
| 735 | const { requestContext: baseContext, memberContext, personId } = await buildRequestContext(event.user, { skipGoals: true }); |
| 736 | |
| 737 | // Record the user's message in the relationship |
| 738 | if (personId) { |
| 739 | relationshipDb.recordPersonMessage(personId, 'slack') |
| 740 | .then(() => relationshipDb.deriveSentiment(personId)) |
| 741 | .catch(error => { |
| 742 | logger.warn({ error, personId }, 'Addie: Failed to record person message (mention)'); |
| 743 | }); |
| 744 | } |
| 745 | |
| 746 | // Add channel guardrails — mentions always happen in channels |
| 747 | const channelGuardrails = [ |
| 748 | '', |
| 749 | '## Channel context', |
| 750 | '**IMPORTANT: This is a channel message visible to all channel members.**', |
| 751 | '- You MUST NOT pitch membership, send join links, or recruit in channels — membership conversations belong in DMs.', |
| 752 | '- You MUST NOT share financial data, member counts, invoice information, individual member details, pricing information, or any other sensitive organizational data.', |
| 753 | ].join('\n'); |
| 754 | const requestContext = baseContext + channelGuardrails; |
| 755 | |
| 756 | // Add admin prefix to user message if applicable |
| 757 | const userMessage = isAAOAdmin ? `[ADMIN USER] ${inputValidation.sanitized}` : inputValidation.sanitized; |
| 758 | |
| 759 | // Check for sensitive topics before processing (channel mentions are more public) |
| 760 | const sensitiveCheck = await checkForSensitiveTopics( |
| 761 | inputValidation.sanitized, |
| 762 | event.user, |
| 763 | event.channel |
| 764 | ); |
| 765 | |
| 766 | // If we should deflect, return the deflection response instead of processing |
| 767 | let response; |
| 768 | if (sensitiveCheck.shouldDeflect && sensitiveCheck.deflectResponse) { |
no test coverage detected