fix(gateway): image vision analysis + media cache lock failures

Two root causes behind 'all image analysis failing':

1. imageResizer still emitted lossless PNG for vision input. A 1024px
   Facebook photo balloons to multi-MB PNG base64 that the vision model
   silently rejects ('Vision API null response'). Switch to JPEG q85
   (no upscaling) — same photo drops to ~100-400KB, model processes fine.
   Re-encodes even already-small images so raw originals never bloat the
   data URL. Added tests/imageResizer.test.ts covering both cases.

2. acquireMediaAnalysisLock INSERT aborted with 'index row requires N
   bytes, maximum size is 8191'. text_analysis_cache.text is the PK in a
   B-tree index (8191-byte/row cap); callers pass the raw image URL as the
   key, and base64 data URLs / very long URLs blow past the limit, so the
   lock INSERT fails and every media analysis is skipped. Hash the URL in
   makeImageCacheKey (image:<sha256[:32]>) — fixed-length, deterministic,
   well under the limit. All store/get/lock/delete callers already route
   through this function so lookup stays consistent.
This commit is contained in:
asepharyana
2026-08-15 23:04:52 +07:00
parent 17a4fbd73d
commit 9c83ec86cc
3 changed files with 80 additions and 28 deletions
@@ -77,14 +77,16 @@ export function makeCustomEmojiCacheKey(emojiId: string): string {
* (different URLs) never collide.
*/
export function makeImageCacheKey(imageUrl: string): string {
try {
const u = new URL(imageUrl);
u.search = "";
u.hash = "";
return `image:${u.toString()}`;
} catch {
return `image:${imageUrl}`;
}
// Hash the URL to a fixed-length key. The raw Discord CDN URL is short,
// but callers sometimes pass base64 data URLs (can be multi-MB) or very
// long signed/external URLs. text_analysis_cache.text is the PK and lives
// in a B-tree index with an 8191-byte per-row limit — inserting a long URL
// as the key aborts the whole INSERT ("index row requires N bytes, maximum
// size is 8191"), which fails acquireMediaAnalysisLock and silently skips
// every media analysis. A 32-char sha256 keeps the key well under the limit
// and is still deterministic (same attachment → same key).
const hash = createHash("sha256").update(imageUrl).digest("hex").slice(0, 32);
return `image:${hash}`;
}
/**