fix(concurrency): eliminate all identified double-queue and race conditions
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.
This commit is contained in:
@@ -78,8 +78,13 @@ export function useMessages() {
|
||||
}, [cursor, loadingMore]);
|
||||
|
||||
const reanalyze = useCallback(async (id: string): Promise<void> => {
|
||||
setMessages((prev) =>
|
||||
prev.map((message) =>
|
||||
// Capture prior state inside the functional updater so we don't need
|
||||
// `messages` as a useCallback dependency (avoids stale closure churn).
|
||||
let saved: MessageRecord | undefined;
|
||||
|
||||
setMessages((prev) => {
|
||||
saved = prev.find((m) => m.id === id);
|
||||
return prev.map((message) =>
|
||||
message.id === id
|
||||
? {
|
||||
...message,
|
||||
@@ -88,11 +93,24 @@ export function useMessages() {
|
||||
ai_analysis: null,
|
||||
}
|
||||
: message,
|
||||
),
|
||||
);
|
||||
await reanalyzeMessage(id);
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await reanalyzeMessage(id);
|
||||
} catch (err) {
|
||||
// HTTP failed — revert the optimistic update so the UI stays truthful.
|
||||
if (saved) {
|
||||
const snapshot = saved;
|
||||
setMessages((prev) =>
|
||||
prev.map((message) => (message.id === id ? snapshot : message)),
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const reanalyzeAllErrors = useCallback(async (): Promise<number> => {
|
||||
// Optimistically mark all error messages as pending
|
||||
setMessages((prev) =>
|
||||
|
||||
Reference in New Issue
Block a user