Implements full audit from double_queue_audit.md.
## Critical
fix(backend): reanalyze-batch — per-scope in-flight guard (messages.routes.ts)
Two concurrent admin sessions clicking 'Retry All Errors' simultaneously now
get a 409 REANALYZE_BATCH_IN_PROGRESS for the same guildId:channelId scope.
Prevents the recovery worker from being triggered twice for the same set of
error messages.
fix(discord-gateway): messageUpdate embed resolution skip (messageCapture.ts)
Discord fires messageUpdate when link previews resolve 1-2s after send
even though the message body is unchanged. Compare newContent vs
existingContent before resetting ai_status to pending and re-queueing LLM.
Eliminates a spurious duplicate analysis that could overwrite a valid result.
## High
fix(discord-gateway): scheduleAutoDelete idempotency (aiAnalyzer.ts)
Add autoDeleteInFlight Set. Both processBatch and processIndividualFallback
call scheduleAutoDelete; without the guard, a message that races through
both paths launches two concurrent attemptAutoDeleteFlaggedMessage calls,
producing a duplicate moderation-action log entry and a Discord 10008 error.
The Set is cleaned up in a .finally() block after each attempt completes.
## Medium
fix(frontend): revert optimistic pending state on HTTP failure (useMessages.ts)
Capture the prior MessageRecord inside the setMessages functional updater
(no extra useCallback deps needed). If reanalyzeMessage() throws, restore
the snapshot so the UI reflects the real DB state instead of lying.
fix(discord-gateway): exclude individual_analysis_exhausted from recovery queries
Both getConversationKeysWithIncompleteAnalysis and
getIncompleteMessagesByConversation now add a NOT LIKE guard for
individual_analysis_exhausted. Prevents an infinite recovery loop if a bug
ever writes both flags to the same row.
## Low
fix(discord-gateway): unified timer path in scheduleConversationAnalysis
Remove the separate cooldown-path that created a secondary timer calling
scheduleConversationAnalysis recursively. Replace with a single
clear-and-reset pattern where delayMs = max(cooldownRemainder+500, debounce).
Eliminates the edge case where both timers were live simultaneously.
## Misc
fix(backend): cast req.params.id to String() to satisfy Express typings
Pre-existing tsc error (string | string[]) exposed by our edit.
String() is correct; route params are always scalar strings at runtime.