← back to Wallco Ai
fix: distinguish ComfyUI-unreachable from quality-gate failure in batch error
380a420f11748bc615aecafc1b3d2c22a94e5d31 · 2026-06-01 16:04:38 -0700 · steve@designerwallcoverings.com
When genComfy() gets a curl connection error (exit 6/7/28/35 = refused/timeout),
it now throws BackendUnreachableError instead of the generic "Command failed: curl"
Error. main() catches it, emits a BACKEND_UNREACHABLE marker to the log, and breaks
immediately (skips the remaining N-1 render attempts). The /api/generator/batch route
scans the log for that marker and reports "render backend (ComfyUI ...) unreachable
— check that ComfyUI is running on Mac1 or set WALLCO_COMFY_FALLBACK=replicate"
instead of the misleading "0/N designs survived the quality gate (code 1)".
Also adds: WALLCO_COMFY_FALLBACK=replicate env flag that triggers an automatic
comfy→Replicate fallback when ComfyUI is down and a Replicate token is available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Files touched
M scripts/generate_designs.jsM server.js
Diff
commit 380a420f11748bc615aecafc1b3d2c22a94e5d31
Author: steve@designerwallcoverings.com <steve@designerwallcoverings.com>
Date: Mon Jun 1 16:04:38 2026 -0700
fix: distinguish ComfyUI-unreachable from quality-gate failure in batch error
When genComfy() gets a curl connection error (exit 6/7/28/35 = refused/timeout),
it now throws BackendUnreachableError instead of the generic "Command failed: curl"
Error. main() catches it, emits a BACKEND_UNREACHABLE marker to the log, and breaks
immediately (skips the remaining N-1 render attempts). The /api/generator/batch route
scans the log for that marker and reports "render backend (ComfyUI ...) unreachable
— check that ComfyUI is running on Mac1 or set WALLCO_COMFY_FALLBACK=replicate"
instead of the misleading "0/N designs survived the quality gate (code 1)".
Also adds: WALLCO_COMFY_FALLBACK=replicate env flag that triggers an automatic
comfy→Replicate fallback when ComfyUI is down and a Replicate token is available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
scripts/generate_designs.js | 95 ++++++++++++++++++++++++++++++++++++++++-----
server.js | 17 +++++++-
2 files changed, 101 insertions(+), 11 deletions(-)
diff --git a/scripts/generate_designs.js b/scripts/generate_designs.js
index 059bf28..f3854e4 100644
--- a/scripts/generate_designs.js
+++ b/scripts/generate_designs.js
@@ -298,6 +298,44 @@ function genReplicate(prompt, seed, outPath, meta) {
return outPath;
}
+// ---- BackendUnreachableError -----------------------------------------------
+// Distinct error class thrown when the render backend (ComfyUI or Replicate)
+// cannot be contacted — connection refused, timeout, DNS failure. This is NOT
+// a quality-gate rejection; the batch route and main() loop use the class tag
+// to emit a clear "backend unreachable" message rather than the misleading
+// "0/N designs survived the quality gate" string.
+//
+// curl exit codes that mean "server not there":
+// 6 = could not resolve host
+// 7 = connection refused
+// 28 = operation timed out
+// 35 = SSL handshake failed (also a network failure for our purposes)
+class BackendUnreachableError extends Error {
+ constructor(backend, url, curlExitCode) {
+ super(`render backend (${backend} ${url}) unreachable (curl exit ${curlExitCode})`);
+ this.name = 'BackendUnreachableError';
+ this.backend = backend;
+ this.backendUrl = url;
+ this.curlExitCode = curlExitCode;
+ }
+}
+const CURL_UNREACHABLE_CODES = new Set([6, 7, 28, 35]);
+
+// Wrap execSync curl calls that talk to ComfyUI so that connection-level
+// failures (curl exits 6/7/28/35) throw BackendUnreachableError instead of
+// the generic "Command failed: curl …" Error that gets mistaken for a
+// quality-gate rejection downstream.
+function comfyCurl(curlCmd, opts, backendUrl) {
+ try {
+ return execSync(curlCmd, opts);
+ } catch (err) {
+ if (CURL_UNREACHABLE_CODES.has(err.status)) {
+ throw new BackendUnreachableError('ComfyUI', backendUrl, err.status);
+ }
+ throw err;
+ }
+}
+
// ---- ComfyUI backend ------------------------------------------------------
// Submits a SDXL txt2img workflow to ComfyUI at COMFY_URL (default Mac1 192.168.1.133:8188).
// Polls /history/<prompt_id> for the result, then pulls the image bytes.
@@ -418,11 +456,12 @@ function genComfy(prompt, seed, outPath, meta) {
}
}
- // Submit
- const submit = execSync(`curl -sf -m 30 -H 'Content-Type: application/json' -X POST '${COMFY}/prompt' -d @-`, {
+ // Submit — use comfyCurl so a connection-refused/timeout throws
+ // BackendUnreachableError instead of a generic "Command failed" message.
+ const submit = comfyCurl(`curl -sf -m 30 -H 'Content-Type: application/json' -X POST '${COMFY}/prompt' -d @-`, {
input: JSON.stringify({ prompt: workflow, client_id: 'wallco-ai' }),
encoding: 'utf8'
- });
+ }, COMFY);
let promptId;
try { promptId = JSON.parse(submit).prompt_id; } catch { throw new Error('ComfyUI submit returned non-JSON: ' + submit.slice(0, 200)); }
if (!promptId) throw new Error('ComfyUI did not return prompt_id');
@@ -450,7 +489,7 @@ function genComfy(prompt, seed, outPath, meta) {
// Pull the first image
const im = images[0];
const viewUrl = `${COMFY}/view?filename=${encodeURIComponent(im.filename)}&subfolder=${encodeURIComponent(im.subfolder || '')}&type=${encodeURIComponent(im.type || 'output')}`;
- execSync(`curl -sf -m 60 -o ${JSON.stringify(outPath)} ${JSON.stringify(viewUrl)}`);
+ comfyCurl(`curl -sf -m 60 -o ${JSON.stringify(outPath)} ${JSON.stringify(viewUrl)}`, {}, COMFY);
if (!fs.existsSync(outPath) || fs.statSync(outPath).size < 1000) {
throw new Error(`ComfyUI image pull failed (${viewUrl})`);
}
@@ -517,11 +556,41 @@ function generate(prompt, seed, outPath, meta) {
if (shouldCircularPad(meta) && backend !== 'comfy') {
backend = 'comfy';
}
- switch (backend) {
- case 'replicate': genReplicate(prompt, seed, outPath, meta); break;
- case 'flux': genReplicateFlux(prompt, seed, outPath, meta); break;
- case 'comfy': genComfy(prompt, seed, outPath, meta); break;
- default: genStub(prompt, seed, outPath, meta); break;
+
+ // Helper: attempt one backend; if it throws BackendUnreachableError and
+ // WALLCO_COMFY_FALLBACK=replicate is set, automatically re-try on Replicate.
+ // The fallback produces non-circular-padded tiles (seam quality is degraded
+ // vs comfy), but it's better than a complete failure when Mac1 is offline.
+ // NOTE: circular-padding quality advantage is lost on the fallback path;
+ // the seamless gate will still run and may reject the result.
+ function callBackend(b) {
+ switch (b) {
+ case 'replicate': return genReplicate(prompt, seed, outPath, meta);
+ case 'flux': return genReplicateFlux(prompt, seed, outPath, meta);
+ case 'comfy': return genComfy(prompt, seed, outPath, meta);
+ default: return genStub(prompt, seed, outPath, meta);
+ }
+ }
+
+ try {
+ callBackend(backend);
+ } catch (err) {
+ if (err instanceof BackendUnreachableError) {
+ // Check for optional comfy→replicate fallback (WALLCO_COMFY_FALLBACK=replicate).
+ const fallback = process.env.WALLCO_COMFY_FALLBACK;
+ if (backend === 'comfy' && fallback === 'replicate') {
+ const tok = loadReplicateToken();
+ if (tok) {
+ console.warn(` ⚠ ComfyUI unreachable — falling back to Replicate (seam quality degraded)`);
+ genReplicate(prompt, seed, outPath, meta);
+ // Re-tag what actually ran so downstream logs show "replicate" not "comfy"
+ return; // success via fallback; post-process below continues normally
+ }
+ }
+ // No fallback available — re-throw so main() can emit the BACKEND_UNREACHABLE marker
+ throw err;
+ }
+ throw err;
}
// 2026-05-22 — Ghost-layer post-process. SDXL produces multi-opacity ghost
// copies of repeat motifs ~100% of the time even with strong anti-prompts
@@ -799,6 +868,14 @@ async function main() {
console.log(` ✓ #${id} · ${thisWidth}" · seed ${seed} · ${path.basename(outPath)} · ©`);
created.push(id);
} catch (e) {
+ if (e instanceof BackendUnreachableError) {
+ // Emit a machine-parseable marker so the batch-route log-scanner in
+ // server.js can distinguish "backend down" from a quality-gate reject.
+ console.log(` ✗ BACKEND_UNREACHABLE ${e.message}`);
+ // A downed backend will fail every subsequent render in this batch too;
+ // break out immediately rather than spinning N more curl timeouts.
+ break;
+ }
console.log(` ✗ ${e.message}`);
}
}
diff --git a/server.js b/server.js
index 14d8db9..ed4c193 100644
--- a/server.js
+++ b/server.js
@@ -4828,7 +4828,7 @@ app.get('/trade/login', (req, res) => {
})}
<body>
${htmlHeader('/trade/login')}
-<main class="prose-main" style="max-width:480px;margin:48px auto;padding:0 24px">
+<main class="prose-main" style="max-width:480px;margin:0 auto;padding:105px 24px 72px">
<article class="prose">
<h1 style="font:300 32px/1.2 'Cormorant Garamond',serif;margin:0 0 8px">Trade sign-in</h1>
<p style="color:var(--ink-soft,#555);font:14px/1.5 var(--sans,system-ui);margin:0 0 24px">
@@ -21658,8 +21658,16 @@ app.post('/api/generator/batch', (req, res) => {
// generator (e.g. the drunk_animals cron) wrote last. Marking by global
// max id mis-attributed another job's design to this run.
let createdIds = [];
+ let backendUnreachableMsg = null;
try {
const logText = fs.readFileSync(logFile, 'utf8');
+ // Detect backend-unreachable before anything else — generate_designs.js
+ // emits "BACKEND_UNREACHABLE render backend (ComfyUI …) unreachable"
+ // when genComfy() gets curl exit 6/7/28/35. This is NOT a quality-gate
+ // rejection and must not be reported as one.
+ const unreach = logText.match(/BACKEND_UNREACHABLE\s+(.+)/);
+ if (unreach) backendUnreachableMsg = unreach[1].trim();
+
const m = logText.match(/Created\s+\d+\/\d+\s+designs\s*·?\s*IDs?:\s*([0-9,\s]+)/i);
if (m) createdIds = m[1].split(',').map(s => parseInt(s.trim(), 10)).filter(Number.isFinite);
// FALLBACK (2026-06-01): a child killed by signal (timeout / pm2 wipe /
@@ -21691,7 +21699,12 @@ app.post('/api/generator/batch', (req, res) => {
const firstId = createdIds[0] || null;
let errMsg = null;
if (!ok) {
- if (killed) {
+ if (backendUnreachableMsg) {
+ // generate_designs.js detected a connection-level failure (curl exit
+ // 6/7/28/35) and emitted a BACKEND_UNREACHABLE marker — surface that
+ // clearly so it's never confused with a quality-gate rejection.
+ errMsg = backendUnreachableMsg + ' — check that ComfyUI is running on Mac1 or set WALLCO_COMFY_FALLBACK=replicate';
+ } else if (killed) {
errMsg = `batch child killed by ${signal || 'signal'} before any design landed `
+ `(timeout/pm2-wipe/OOM — NOT a quality-gate rejection)`;
} else if (createdIds.length === 0) {
← 9f255b7 designs: move social-share ↗ FAB right of the bulk-select ch
·
back to Wallco Ai
·
deploy: ship generate_designs.js + generator_tick.js in 2d r d0e305b →