From 6effb51b5deef5029926185c985b4a3f5cf79fa5 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Fri, 12 Jun 2026 18:45:33 +0700 Subject: [PATCH] fix(ai-moderation): don't cache text-only results for messages with media - hasMediaContent now also checks evidence.attachments from metadata (not just DB attachment records), catching the race where attachment DB rows aren't inserted yet when analysis runs. - Cache-hit guard: treat cached entries as miss when the message has media evidence in metadata, so stale 24h-freezes are avoided. - Cache-write guard: skip storing text-only analysis results for messages whose metadata shows attachments/stickers/embeds. This prevents a text-only 'clean' result (from failed vision) being frozen for 24h, blocking future re-analysis with full media context. Co-Authored-By: Claude --- .../ai-moderation/llmModerationClient.ts | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts index bf71758..37e25fb 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts @@ -151,7 +151,14 @@ function hasMediaContent( ): boolean { if (target.metadata) { const evidence = extractMessageMediaEvidence(target.metadata); - if (evidence.stickers.length > 0 || evidence.embeds.length > 0) return true; + // Check all media types from metadata — attachments in particular are + // captured at message-creation time so they exist before the DB record. + if ( + evidence.stickers.length > 0 || + evidence.embeds.length > 0 || + evidence.attachments.length > 0 + ) + return true; } if (attachments?.some((a) => a.message_id === target.id)) return true; return false; @@ -1206,7 +1213,26 @@ export async function runModerationAnalysis( // Safety: skip cache entries that are artifacts of API/parse errors. // A previous bug cached error results as "flagged", causing 24h false positives. // This guards against both legacy corrupt entries and any future write-path bugs. - if ( + const hasMediaInMeta = + target.metadata && + (() => { + const ev = extractMessageMediaEvidence(target.metadata); + return ( + ev.attachments.length > 0 || + ev.stickers.length > 0 || + ev.embeds.length > 0 + ); + })(); + + if (hasMediaInMeta) { + log.debug( + { + messageId: target.id, + cacheKey, + }, + "Cache entry exists but message has media in metadata — treating as miss", + ); + } else if ( cached.flags.some((f) => [ "analysis_api_failed", @@ -1315,6 +1341,31 @@ export async function runModerationAnalysis( // Caching a transient error would turn it into a 24h false positive. if (result.status === "error") continue; + // Do NOT cache text-only analysis for messages with media evidence in + // metadata (attachments, stickers, embeds). A complete analysis needs + // full media context, and caching a text-only result would prevent future + // media-aware re-analysis. The attachment DB record may not exist yet + // due to a race condition, so we check the message's own metadata field. + if (target.metadata) { + const evidence = extractMessageMediaEvidence(target.metadata); + if ( + evidence.attachments.length > 0 || + evidence.stickers.length > 0 || + evidence.embeds.length > 0 + ) { + log.debug( + { + messageId: target.id, + attachments: evidence.attachments.length, + stickers: evidence.stickers.length, + embeds: evidence.embeds.length, + }, + "Skipping cache for text-only result — message has media evidence in metadata", + ); + continue; + } + } + const cacheKey = makeTextModerationCacheKey(rawContent); setCachedTextModeration(cacheKey, { flags: result.flags ?? [],