diff --git a/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts b/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts index f82b3b3..e7138eb 100644 --- a/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts +++ b/services/discord-gateway/src/modules/ai-moderation/aiAnalyzer.ts @@ -272,7 +272,7 @@ let activeIndividualRequests = 0; // Individual fallback circuit breaker (independent of batch CB) let individualConsecutiveErrors = 0; let individualCooldownUntil = 0; -const INDIVIDUAL_COOLDOWN_MS = 30000; +const INDIVIDUAL_COOLDOWN_MS = 60000; // --------------------------------------------------------------------------- // Piscina worker pool (batch path only) @@ -851,10 +851,13 @@ async function processBatch( // Trigger conversation cooldown so we don't tight loop the API recordConversationBatchFailure(conversationKey); - conversationErrorCooldown.set( - conversationKey, - Date.now() + config.AI_ANALYSIS_ERROR_COOLDOWN_MS, - ); + // Preserve the longer cooldown: circuit breaker (via recordConversationBatchFailure) + // may have set a 60s cooldown; don't let the shorter config value overwrite it. + const existingCooldown = conversationErrorCooldown.get(conversationKey) ?? 0; + const newCooldown = Date.now() + config.AI_ANALYSIS_ERROR_COOLDOWN_MS; + if (newCooldown > existingCooldown) { + conversationErrorCooldown.set(conversationKey, newCooldown); + } // FIX: Release the processing lock immediately so the cooldown timer // (not the processing-timeout expiry) controls when this conversation @@ -890,10 +893,13 @@ async function processBatch( lastError = error instanceof Error ? error.message : String(error); const errorStack = error instanceof Error ? error.stack : undefined; - conversationErrorCooldown.set( - conversationKey, - Date.now() + config.AI_ANALYSIS_ERROR_COOLDOWN_MS, - ); + // Preserve the longer cooldown: circuit breaker (via recordConversationBatchFailure) + // may have set a 60s cooldown; don't let the shorter config value overwrite it. + const existingCatchCooldown = conversationErrorCooldown.get(conversationKey) ?? 0; + const newCatchCooldown = Date.now() + config.AI_ANALYSIS_ERROR_COOLDOWN_MS; + if (newCatchCooldown > existingCatchCooldown) { + conversationErrorCooldown.set(conversationKey, newCatchCooldown); + } logger.error( { conversationKey, diff --git a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts index 4a2838e..f9b629f 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmClient.ts @@ -165,9 +165,9 @@ export async function llmChat( }, { retries, - minTimeout: 500, - maxTimeout: 4_000, - factor: 2, + minTimeout: 2_000, + maxTimeout: 30_000, + factor: 3, }, ); } diff --git a/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts b/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts index 17f16a0..e32d16a 100644 --- a/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts +++ b/services/discord-gateway/src/modules/ai-moderation/llmModerationClient.ts @@ -679,8 +679,8 @@ const analyzeSingleMediaImage = async ( lastError = err instanceof Error ? err : new Error(String(err)); if (attempt < 2) { const backoffMs = Math.min( - 1_000 * 2 ** attempt + Math.random() * 200, - 8_000, + 2_000 * 3 ** attempt + Math.random() * 500, + 30_000, ); log.warn( { @@ -824,9 +824,27 @@ async function callModerationLLM( throw parseError; } } catch (apiError: any) { - // 429/401/403 → abort immediately, never retry + // 429 → retryable with backoff (rate limited — the provider may recover) + // GitHub Issue #429-cascade: 429 was previously an AbortError which skipped + // retries and caused immediate re-queues, creating a tight rate-limit cascade. + // Now treated as retryable with generous backoff so the provider can recover. + if (apiError?.status === 429) { + log.warn( + { + status: 429, + targetIds, + model: config.AI_LLM_MODEL, + label, + }, + "LLM API 429 rate limited — will retry with backoff", + ); + // Add a small jitter to prevent thundering herd on retry + const jitterMs = Math.floor(Math.random() * 1000) + 500; + await delay(jitterMs); + throw apiError; + } + // 401/403 → abort immediately, never retry if ( - apiError?.status === 429 || apiError?.status === 401 || apiError?.status === 403 ) { @@ -848,10 +866,10 @@ async function callModerationLLM( } }, { - retries: 2, - minTimeout: 3000, - maxTimeout: 8000, - factor: 2, + retries: 3, + minTimeout: 5_000, + maxTimeout: 60_000, + factor: 3, }, ); parsed = analysis.parsed; diff --git a/services/discord-gateway/src/modules/ai-moderation/moderationPrompt.ts b/services/discord-gateway/src/modules/ai-moderation/moderationPrompt.ts index 886687a..46097c0 100644 --- a/services/discord-gateway/src/modules/ai-moderation/moderationPrompt.ts +++ b/services/discord-gateway/src/modules/ai-moderation/moderationPrompt.ts @@ -41,12 +41,13 @@ Pedoman ini mencerminkan nilai-nilai yang dijunjung server. Terapkan dengan bija - **Diskriminasi penampilan fisik** (mis. "gendut", "iteman", "cungkring") → "harassment" jika terarah ke individu. - Pelecehan, rasisme, seksisme, dan segala bentuk diskriminasi lainnya tidak ditoleransi. - Perbedaan pendapat itu wajar. Serangan personal, penghinaan, dan merendahkan orang lain tidak. +- **Toxicity pada Entitas Eksternal:** Makian/trash-talk emosional yang ditujukan pada benda mati, game, karakter fiksi, perusahaan, atau konsep (mis. "game ini ampas", "dev bodoh") adalah AMAN. Harassment dan hate_speech HANYA berlaku jika ditujukan pada anggota/kelompok server secara personal. - **Pelecehan Agama / Candaan Berlebihan:** Dilarang keras menjadikan tata cara ibadah (seperti sholat), simbol agama, atau ritual sebagai bahan candaan satir, meme, atau parodi (contoh: sholat di depan foto Stalin, menggabungkan ibadah dengan tokoh/entitas tidak pantas). Meskipun konteksnya humor atau satir, "candaan berlebihan" terkait agama WAJIB di-flag sebagai "sara". ### Anti-Evasion & Obfuscation (STRICT RULE) -- **Zalgo / Leetspeak / Simbol:** Pesan yang menggunakan karakter simbolik acak, Zalgo text, atau leetspeak (misal: "++++++K1[[ your $€/F", "b1tch", "k0nt0l") adalah TEKNIK EVASI. JANGAN PERNAH menganggapnya sebagai "pesan abstrak", "typo", atau "tanpa makna". KAMU WAJIB mendekode/membaca makna aslinya. Jika maknanya merujuk pada "kill yourself", "die", ancaman, atau kata kasar, FLAG sebagai "harassment" atau "hate_speech" dengan severity "high" atau "critical". +- **Zalgo / Leetspeak / Simbol:** Pesan yang menggunakan karakter simbolik acak, Zalgo text, atau leetspeak (misal: "++++++K1[[ your $€/F", "b1tch", "k0nt0l") adalah TEKNIK EVASI. KAMU WAJIB mendekode makna aslinya. Jika maknanya merujuk pada ancaman atau kata kasar, FLAG sebagai "harassment" atau "hate_speech". PENGECUALIAN: Kaomoji (misal ╯°□°)╯︵ ┻━┻) atau ASCII art dekoratif adalah AMAN dan BUKAN teknik evasi. - **Emoji Huruf / Regional Indicators:** Jika pesan menggunakan deretan emoji bendera/huruf (seperti 🇬 🇦 🇾) atau karakter spesial yang dirangkai untuk mengeja sebuah kata, JANGAN menganggapnya sebagai "serangkaian emoji tanpa teks". KAMU WAJIB membaca rangkaian emoji tersebut sebagai huruf dan kata aslinya. Jika kata yang dibentuk melanggar, flag sesuai pelanggarannya. -- **Bahasa Asing & Vulgaritas Obscure:** Kata-kata vulgar atau anatomi seksual dalam bahasa asing/slang (seperti "titten" (German for tits), "nigger", "kys", "whore") ADALAH PELANGGARAN. JANGAN abaikan atau anggap sebagai "bercanda", "tidak serius", atau "percakapan santai" hanya karena menggunakan huruf kapital (mis. "AKU RAJA TITTEN") atau format guyonan. +- **Bahasa Asing & Vulgaritas Obscure:** Kata-kata vulgar atau anatomi seksual dalam bahasa asing/slang (seperti "titten", "nigger", "kys", "whore") ADALAH PELANGGARAN. PENGECUALIAN: Kata asing dengan ejaan mirip namun makna normal (False Friends) seperti penyebutan negara "Niger", warna bahasa Spanyol "negro", atau kata ganti Korea "niga" adalah AMAN. - **Zero Tolerance untuk Evasi & Vulgaritas Seksual:** Setiap indikasi user mencoba menyembunyikan kata kasar di balik simbol, atau menggunakan vulgaritas anatomi seksual secara "santai", HARUS langsung di-flag. Jangan beri toleransi atau label "clean". ### Hindari Konflik dan Jaga Kedamaian @@ -63,21 +64,22 @@ Pedoman ini mencerminkan nilai-nilai yang dijunjung server. Terapkan dengan bija ## Kategori Pelanggaran & Kriteria Flag Prioritas tertinggi (ANCAMAN KESELAMATAN): -- child_safety, self_harm, violence, illegal_content — flag jika ada indikasi nyata -- Pornografi/NSFW, ajakan seksual, roleplay seksual → "sexual_content" +- child_safety, violence, illegal_content — flag jika ada indikasi nyata +- self_harm: BEDAKAN antara ancaman klinis nyata dengan hiperbola stres remaja (mis. "mati aja gua ngerjain tugas", "bunuh aku sekarang"). Hiperbola ekspresi stres adalah AMAN dan JANGAN di-flag sebagai self_harm. +- Pornografi/NSFW, ajakan seksual, roleplay seksual → "sexual_content". PENGECUALIAN: Roleplay aksi kasual/RPG non-seksual (misal \*memeluk teman karena sedih\*, \*menebas naga\*) adalah AMAN dan bukan sexual_content. - Judi/promosi judi → "gambling" - Narkoba/promosi → "drugs" Prioritas menengah (PERILAKU MERUSAK): -- Ancaman kekerasan, doxxing, scam → flag sesuai kategori -- Spam self-promo → "spam" +- Ancaman kekerasan, doxxing, scam → flag sesuai kategori. PENGECUALIAN DOXXING: Pengguna membagikan informasi pribadinya sendiri secara sukarela (self-disclosure, misal perkenalan nama asli/kota) adalah AMAN. +- Spam self-promo → "spam". PENGECUALIAN SPAM: Membagikan link karya/portofolio/repo pribadi untuk membantu menjawab pertanyaan teknis anggota lain adalah AMAN. - Istilah agama/suku/ras: penyebutan netral/edukasi = clean; hinaan/provokasi/diskriminatif = "sara" atau "hate_speech" - **Memancing drama/konflik** → "conflict_instigation" Prioritas rendah (PELANGGARAN RINGAN): - harassment (targeted insult), vulgar_language (profanity terarah) -- sexual_deviation: mempromosikan/mendukung/membahas topik seksual/identitas yang dibatasi server. Ini **TERMASUK MENTION SINGKAT** tentang: BL (Boys Love), yaoi, yuri, pengakuan orientasi seksual non-hetero, pembahasan hubungan sesama jenis, referensi furry, promosi identitas LGBT. Sekalipun hanya "membahas topik BL secara santai", itu TETAP PELANGGARAN dan WAJIB di-flag atau di-warn. JANGAN anggap BL/LGBT sebagai "topik aman tanpa pelanggaran". -- Username/display name ofensif → "offensive_username" (dengan pertimbangan konteks) +- sexual_deviation: mempromosikan/mendukung/membahas topik seksual/identitas yang dibatasi server. Ini **TERMASUK MENTION SINGKAT** tentang: BL (Boys Love), yaoi, yuri, pengakuan orientasi seksual non-hetero, referensi furry fetish, promosi identitas LGBT. PENGECUALIAN: Karakter hewan fiksi antropomorfik normal (seperti Sonic, Pokemon, maskot anime) adalah BUKAN referensi furry fetish. Jangan flag diskusi karakter fiksi hewan normal sebagai sexual_deviation. +- Username/display name ofensif → "offensive_username" (dengan pertimbangan konteks). PENGECUALIAN: Jangan flag username yang memuat badword secara tidak sengaja akibat susunan huruf alami (Scunthorpe problem, misal "Sasuke" aman meski mengandung "asu"). ## Aturan Analisis URL — JANGAN GUESS BERDASARKAN DOMAIN - Jika pesan berisi URL, PERIKSA apakah ada tag [web_content] di pesan tersebut. @@ -105,6 +107,7 @@ Jika ragu antara dua level, pilih yang lebih rendah (innocent until proven guilt ### Mode 1: Teks + Gambar (teks adalah bukti utama) - Jika ada teks percakapan normal ("Aku suka nasgor loh", "Halo guys") → gambar hampir pasti bukan pelanggaran. - Jika teks clean: OVERRIDE klaim vision tentang judi KECUALI ada bukti spesifik (chip, kartu, odds, logo dikenal). +- **Pengecualian Bias NSFW:** Jika vision model mendeskripsikan "wanita berbikini", "seni patung", atau konteks pakaian minim di tempat wajar (pantai, seni klasik), JANGAN flag sebagai sexual_content KECUALI terdapat elemen pornografi eksplisit. - Teks lebih penting dari gambar. ### Mode 2: HANYA GAMBAR (teks kosong/sangat pendek/tidak bermakna) @@ -113,6 +116,7 @@ Jika ragu antara dua level, pilih yang lebih rendah (innocent until proven guilt - Jika deskripsi menyebutkan "terminal", "console", "editor kode" → itu BUKAN gambling. Clean. - Jika deskripsi menyebutkan "aplikasi chat", "screenshot percakapan" → itu BUKAN gambling. Clean. - Jika deskripsi menyebutkan "foto makanan/pemandangan/selfie/hewan" → Clean. +- **Pengecualian Bias NSFW Mode 2:** Sama seperti Mode 1, foto di pantai berbikini atau karya seni non-pornografi bukanlah sexual_content. - **HANYA flag gambling jika deskripsi SECARA EKSPLISIT menyebutkan elemen judi NYATA: chip, kartu remi, meja taruhan, odds, deposit/withdraw, logo situs judi.** - JANGAN abaikan gambar hanya karena teks kosong. Analisis TETAP harus dilakukan berdasarkan deskripsi gambar. - **Jika pesan HANYA berisi gambar tanpa teks → WAJIB membaca Media analysis dan membuat keputusan berdasarkan deskripsi tersebut.**`; diff --git a/services/discord-gateway/src/shared/config/config.ts b/services/discord-gateway/src/shared/config/config.ts index 65801a9..029667b 100644 --- a/services/discord-gateway/src/shared/config/config.ts +++ b/services/discord-gateway/src/shared/config/config.ts @@ -96,7 +96,7 @@ const configSchema = z .number() .positive() .default(15000), - AI_ANALYSIS_ERROR_COOLDOWN_MS: z.coerce.number().positive().default(0), + AI_ANALYSIS_ERROR_COOLDOWN_MS: z.coerce.number().positive().default(30000), /** Max messages fetched per conversation batch (token budget is the real constraint). */ AI_ANALYSIS_MAX_BATCH_SIZE: z.coerce.number().int().positive().default(200), AI_ANALYSIS_MAX_CONTEXT_TOKENS: z.coerce.number().positive().default(8000),