[object Object]

← 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

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 →