fix(ai-moderation): eliminate double-queue on failure and spam-retry

Three root causes patched:

1. aiAnalyzer.ts — processBatch apiFailedMessages path:
   After reverting messages to 'pending', suppress shouldScheduleNext
   (was true by default) to prevent scheduleConversationAnalysis from
   firing immediately and racing with the recovery worker that will pick
   up those same pending messages on its next poll cycle.
   Also release the conversationProcessing lock immediately after the
   revert so the cooldown timer (not the full processing-timeout) gates
   the next attempt.

2. messages.routes.ts — POST /messages/:id/reanalyze:
   Add a per-message reanalyzeInFlight Set.  Concurrent requests for
   the same ID now return HTTP 409 instead of issuing duplicate UPDATEs
   and triggering multiple recovery worker activations.
   Also narrow the SQL predicate to 'WHERE id =  AND ai_status != pending'
   so a click that arrives while the recovery worker already picked the
   message up is a no-op at the DB level.
This commit is contained in:
MythEclipse
2026-06-05 15:38:31 +07:00
parent a0bf7dfdd9
commit 5c0a837cf0
2 changed files with 45 additions and 5 deletions
@@ -800,6 +800,22 @@ async function processBatch(
conversationKey,
Date.now() + config.AI_ANALYSIS_ERROR_COOLDOWN_MS,
);
// FIX: Release the processing lock immediately so the cooldown timer
// (not the processing-timeout expiry) controls when this conversation
// is next eligible. Without this the lock would hold for the full
// AI_ANALYSIS_PROCESSING_TIMEOUT_MS before the recovery worker could
// pick the reverted-pending messages back up.
if (conversationProcessing.get(conversationKey) === processingStartedAt) {
conversationProcessing.delete(conversationKey);
}
// FIX: Do NOT set shouldScheduleNext = true here. The reverted messages
// are now 'pending' again. scheduleConversationAnalysis would race with
// the recovery worker and schedule the same conversation twice — once
// immediately (via shouldScheduleNext) and once after the cooldown
// (via recovery worker). Let the cooldown gate the next attempt.
shouldScheduleNext = false;
}
if (apiFailedMessages.length === 0) {