← back to Enrich Local Hybrid
enricher: dead-image drain guard + tag-on-skip (TK-10168)
11302b0d84d28ee74679d0be3864f474771d3972 · 2026-08-06 14:28:54 -0700 · steve
loadProducts() re-loaded dead-image rows every run (unfetchable URL → fetch fails →
self-skip → stays untagged → re-churned forever). Two additive changes:
1. drain guard: exclude image_rejected IS DISTINCT FROM TRUE from the enrichable queue.
2. tag-on-skip: a genuinely dead image (geminiAnalyze all-candidates-failed) now returns a
DISTINCT {__deadImage} sentinel; the main loop flags image_rejected=TRUE once + continues.
Sentinel avoids the naive-fix bug of mis-tagging 429/no-data nulls as dead images.
Reversible; writes gate on the normal enricher run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M enrich-ai-tags.patched.js
Diff
commit 11302b0d84d28ee74679d0be3864f474771d3972
Author: steve <steve@designerwallcoverings.com>
Date: Thu Aug 6 14:28:54 2026 -0700
enricher: dead-image drain guard + tag-on-skip (TK-10168)
loadProducts() re-loaded dead-image rows every run (unfetchable URL → fetch fails →
self-skip → stays untagged → re-churned forever). Two additive changes:
1. drain guard: exclude image_rejected IS DISTINCT FROM TRUE from the enrichable queue.
2. tag-on-skip: a genuinely dead image (geminiAnalyze all-candidates-failed) now returns a
DISTINCT {__deadImage} sentinel; the main loop flags image_rejected=TRUE once + continues.
Sentinel avoids the naive-fix bug of mis-tagging 429/no-data nulls as dead images.
Reversible; writes gate on the normal enricher run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
enrich-ai-tags.patched.js | 23 ++++++++++++++++++++++-
1 file changed, 22 insertions(+), 1 deletion(-)
diff --git a/enrich-ai-tags.patched.js b/enrich-ai-tags.patched.js
index 248945a..16a8320 100644
--- a/enrich-ai-tags.patched.js
+++ b/enrich-ai-tags.patched.js
@@ -347,7 +347,9 @@ async function geminiAnalyze(imageUrl, mode, attempt = 0) {
}
if (!imageBuffer) {
console.log(` SKIP: Cannot fetch image — all ${candidates.length} candidate URL(s) failed`);
- return null;
+ // TK-10168: DISTINCT dead-image sentinel (not a bare null). The caller tags image_rejected
+ // ONLY on this signal — never on a 429/no-data null, which also reads falsy downstream.
+ return { __deadImage: true };
}
imageUrl = resolvedUrl; // from here on, use the URL that actually fetched
@@ -601,6 +603,10 @@ async function loadProducts(vendorCode, tableName, force, limit) {
if (!force) {
// Handle both text and jsonb column types for ai_tags
conditions.push(`(ai_tags IS NULL OR ai_tags::text = '' OR ai_tags::text = '[]' OR ai_tags::text = 'null')`);
+ // TK-10168 drain guard: rows already flagged image_rejected=TRUE (e.g. dead image URLs,
+ // tagged by the tag-on-skip below) drop out of the enrichable queue so they don't
+ // re-fetch/re-fail/re-churn every run. Paired with the tag-on-skip; a no-op without it.
+ conditions.push(`(image_rejected IS DISTINCT FROM TRUE)`);
}
let sql = `
@@ -748,6 +754,21 @@ async function main() {
continue;
}
+ // TK-10168 tag-on-skip: a genuinely dead image (all candidate URLs unfetchable) is flagged
+ // ONCE so the drain guard permanently excludes it — instead of re-churning forever. Keyed on
+ // the explicit __deadImage sentinel, NEVER on a plain 429/no-data null (which must not be tagged).
+ if (aiData && aiData.__deadImage) {
+ console.log(' → SKIP (dead image — all candidate URLs unfetchable) → tagging image_rejected [TK-10168]');
+ skipped++;
+ try {
+ await pool.query(
+ `UPDATE ${catalogTable} SET image_rejected = TRUE, image_rejection_reason = $2 WHERE id = $1`,
+ [row.id, 'dead_image_url:unfetchable [TK-10168]']
+ );
+ } catch (e) { console.log(` (image_rejected tag write failed: ${e.message})`); }
+ continue;
+ }
+
// circuit-breaker: a success clears the streak; too many quota-429s in a row => stop cleanly
if (aiData) consecutive429 = 0;
else if (consecutive429 >= QUOTA_BREAK_STREAK) {
← 20434f1 chore: macstudio3 migration — reconcile from mac2 + repoint
·
back to Enrich Local Hybrid
·
Add reversible Phase-3 image-less phantom-backlog reconcile 2b5fd92 →