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 <noreply@anthropic.com>
This commit is contained in:
MythEclipse
2026-06-12 18:45:33 +07:00
co-authored by Claude
parent 33b787dbd6
commit 6effb51b5d
@@ -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 ?? [],