perf(ai-moderation): dual-key write-back — clean verdict ikut di-cache global

Analisis pertama tetap berkonteks (chat history) demi akurasi, tapi
verdict clean non-actionable (conf>=0.85) juga ditulis di bare key
tanpa konteks. Repeat teks sama di channel lain -> exact cache HIT,
bukan LLM call baru. Guard sama dgn read path; dedupe LRU per proses;
bare row tanpa embedding (tier semantic sudah global).
This commit is contained in:
asepharyana
2026-08-24 20:18:14 +07:00
parent 1accfd9390
commit ccf3fa260e
3 changed files with 58 additions and 11 deletions
@@ -5,6 +5,7 @@
* parallel text+media analysis, LLM calls with retry, and cache handling.
*/
import { createChildLogger } from "@/shared/logger/index";
import { LRUCache } from "lru-cache";
import { config } from "../../shared/config/config.js";
import { extractMessageMediaEvidence } from "../message-capture/messageMetadata.js";
import type {
@@ -35,6 +36,13 @@ import {
const log = createChildLogger("moderationOrchestrator");
/**
* Bare keys already written this process (dual-key write-back dedupe).
* LRU-bounded so a long-lived gateway can't grow it without limit; the DB
* upsert underneath is idempotent anyway — this just avoids redundant writes.
*/
const globalBareKeysWritten = new LRUCache<string, true>({ max: 5000 });
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
@@ -434,20 +442,50 @@ export async function runModerationAnalysis(
rawContent,
makeModerationContextKey(target),
);
const stored = {
flags: result.flags ?? [],
score: result.score ?? 0,
analysis: result.analysis ?? "",
categories: result.categories ?? result.flags ?? [],
severity: result.severity ?? "none",
confidence: result.confidence ?? result.score ?? 0,
recommendedAction: result.recommendedAction ?? "none",
status: result.status,
};
setCachedTextModeration(
cacheKey,
{
flags: result.flags ?? [],
score: result.score ?? 0,
analysis: result.analysis ?? "",
categories: result.categories ?? result.flags ?? [],
severity: result.severity ?? "none",
confidence: result.confidence ?? result.score ?? 0,
recommendedAction: result.recommendedAction ?? "none",
status: result.status,
},
stored,
embeddingsByKey.get(cacheKey),
).catch(() => {});
// Dual-key write-back (2026-08-24): the FIRST analysis of a message runs
// WITH conversation context (accurate), but its verdict is also stored
// under the context-free bare key so repeats in OTHER channels hit the
// exact cache instead of paying a new LLM call. Same guard as the read
// path — only non-actionable clean verdicts may cross channels. No
// embedding on the bare row: the semantic tier is already global, and
// writing one would create a duplicate Qdrant point for this content.
const bareKey = makeTextModerationCacheKey(rawContent);
if (
bareKey !== cacheKey &&
!globalBareKeysWritten.has(bareKey) &&
isGloballyReusableCleanVerdict(
{
status: stored.status,
flags: stored.flags,
score: stored.score,
analysis: stored.analysis,
categories: stored.categories,
severity: stored.severity,
confidence: stored.confidence,
recommendedAction: stored.recommendedAction,
},
undefined,
)
) {
globalBareKeysWritten.set(bareKey, true);
setCachedTextModeration(bareKey, stored, null).catch(() => {});
}
}
const allResults = [
@@ -524,7 +524,7 @@ export function isSemanticBandAccepted(
* globally reused — enforcement is context-sensitive by design.
*/
export function isGloballyReusableCleanVerdict(
verdict: StoredModerationVerdict,
verdict: Omit<StoredModerationVerdict, "status"> & { status: string },
analyzedAtMs: number | undefined,
): boolean {
if (verdict.status !== "clean") return false;
@@ -157,4 +157,13 @@ describe("isGloballyReusableCleanVerdict", () => {
it("skips the age check when analyzedAt is unknown", () => {
expect(isGloballyReusableCleanVerdict(makeVerdict(), undefined)).toBe(true);
});
it("rejects non-standard statuses such as processing write-backs", () => {
expect(
isGloballyReusableCleanVerdict(
{ ...makeVerdict(), status: "processing" },
undefined,
),
).toBe(false);
});
});