Root cause (trace msg 1541417073245290638): - Race-guard upload-pending balik results:[] diperlakukan sbg SUKSES -> row yatam 'processing' sampai cleanup 300s mengembalikan - Vision gagal 3x utk GIF besar (SSE truncation) tanpa fallback Fix: - Sinyal eksplisit uploadPending dari worker race guard - Classifier murni classifyIndividualWorkerResult(): upload_pending -> requeue pending + reschedule segera (250ms), bukan error palsu; empty-results ok:true kini error transien (bug silent-success mati) - llmVision fallback stream:false sekali saat SSE truncation - Safety-net cleanup stuck processing 300s -> 120s
66 lines
2.3 KiB
TypeScript
66 lines
2.3 KiB
TypeScript
/**
|
|
* fallbackResultClassifier.ts
|
|
*
|
|
* Pure classifier for the individual-fallback worker response.
|
|
*
|
|
* Bug history (2026-08-24): the worker's upload-pending race guard returned
|
|
* `{ ok: true, results: [] }` (a legacy "no results yet" signal), but the
|
|
* processor treated ANY `ok:true` as a successful moderation. Empty results
|
|
* meant nothing was written to the DB — the message stayed stuck in
|
|
* `ai_status='processing'` with nobody watching it until the 300s cleanup
|
|
* reverted it. That single gap produced the ~330-400s attachment delay
|
|
* cluster. Classification now happens in ONE pure function so every outcome
|
|
* has an explicit, testable owner.
|
|
*/
|
|
|
|
export type WorkerResultKind =
|
|
| "success"
|
|
| "upload_pending"
|
|
| "incomplete"
|
|
| "error";
|
|
|
|
export interface ClassifiableWorkerResult {
|
|
ok?: boolean;
|
|
/** Upload-pending marker set by ai-analysis-worker's race guard. */
|
|
uploadPending?: boolean;
|
|
results?: Array<{ status?: string; flags?: string[] | string } | undefined>;
|
|
error?: string;
|
|
}
|
|
|
|
function flagsOf(r: { flags?: string[] | string }): string[] {
|
|
if (!r.flags) return [];
|
|
if (Array.isArray(r.flags)) return r.flags;
|
|
try {
|
|
const parsed = JSON.parse(r.flags) as unknown;
|
|
return Array.isArray(parsed) ? (parsed as string[]) : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Classify an individual-fallback worker response:
|
|
* - "upload_pending": explicit race-guard signal — retry shortly, NOT an error.
|
|
* - "success": at least one result and none is analysis_incomplete.
|
|
* - "incomplete": LLM ran but dropped/failed this message after retries
|
|
* (analysis_incomplete flag) — terminal exhausted path.
|
|
* - "error": anything else (ok:false, or ok:true with NO explainable
|
|
* results). The old code silently succeeded here — never again.
|
|
*/
|
|
export function classifyIndividualWorkerResult(
|
|
result: ClassifiableWorkerResult,
|
|
): WorkerResultKind {
|
|
if (result.uploadPending === true) return "upload_pending";
|
|
const results = (result.results ?? []).filter(
|
|
(r): r is NonNullable<typeof r> => Boolean(r),
|
|
);
|
|
if (results.length === 0) return "error";
|
|
if (result.ok !== true) return "error";
|
|
for (const r of results) {
|
|
const flags = flagsOf(r);
|
|
if (flags.includes("analysis_incomplete")) return "incomplete";
|
|
if ((r.status ?? "") === "") return "error";
|
|
}
|
|
return "success";
|
|
}
|