[object Object]

← back to Marketing Command Center

engine: make GET /today non-blocking + add generation run-lock

5869a28b656d00208d434493807a44a032cf91b4 · 2026-08-15 20:38:17 -0700 · Steve Abrams

GET /api/engine/today would run the full local-LLM caption batch inline
(sequential candidates x qwen3 vision+chat, 45s/90s per-call timeouts), so
the default endpoint blocked for minutes. The panel already dodged this via
?generate=0, but the bare endpoint hung any direct/health caller.

- generate.js: coalesce concurrent generateToday() onto one in-flight run
  (run-lock) + export isGenerating(); prevents parallel batches hammering Ollama.
- index.js /today: default now kicks generation in the background and returns
  current items immediately with {generating}. ?wait=1 keeps the old sync
  behavior; ?generate=0 (panel poll) unchanged.

Files touched

Diff

commit 5869a28b656d00208d434493807a44a032cf91b4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 15 20:38:17 2026 -0700

    engine: make GET /today non-blocking + add generation run-lock
    
    GET /api/engine/today would run the full local-LLM caption batch inline
    (sequential candidates x qwen3 vision+chat, 45s/90s per-call timeouts), so
    the default endpoint blocked for minutes. The panel already dodged this via
    ?generate=0, but the bare endpoint hung any direct/health caller.
    
    - generate.js: coalesce concurrent generateToday() onto one in-flight run
      (run-lock) + export isGenerating(); prevents parallel batches hammering Ollama.
    - index.js /today: default now kicks generation in the background and returns
      current items immediately with {generating}. ?wait=1 keeps the old sync
      behavior; ?generate=0 (panel poll) unchanged.
---
 modules/engine/generate.js | 15 +++++++++++++--
 modules/engine/index.js    | 32 ++++++++++++++++++++++----------
 2 files changed, 35 insertions(+), 12 deletions(-)

diff --git a/modules/engine/generate.js b/modules/engine/generate.js
index d69d6cd..6702c8d 100644
--- a/modules/engine/generate.js
+++ b/modules/engine/generate.js
@@ -80,7 +80,18 @@ function buildItem(product, channel, caps, dateStr) {
 }
 
 // generateToday() — idempotent daily generation. Returns today's items.
-async function generateToday() {
+// A module-level run-lock coalesces concurrent callers (two "Generate" clicks, a
+// manual /generate + a background /today kick, etc.) onto ONE in-flight batch, so
+// the local Ollama is never hammered by parallel caption runs for the same day.
+let _inFlight = null;
+function generateToday() {
+  if (_inFlight) return _inFlight;
+  _inFlight = _generateToday().finally(() => { _inFlight = null; });
+  return _inFlight;
+}
+function isGenerating() { return _inFlight != null; }
+
+async function _generateToday() {
   const dateStr = today();
 
   // idempotent: today already generated → return as-is. But if a NEW channel was
@@ -133,4 +144,4 @@ async function generateToday() {
   return store.load().filter(it => it.date === dateStr);
 }
 
-module.exports = { generateToday };
+module.exports = { generateToday, isGenerating };
diff --git a/modules/engine/index.js b/modules/engine/index.js
index ca1b7e9..84fe7f6 100644
--- a/modules/engine/index.js
+++ b/modules/engine/index.js
@@ -69,22 +69,34 @@ module.exports = {
       });
     });
 
-    // GET /today — today's suggestions. If none exist and generate isn't disabled
-    // (?generate=0), ask the generator to build them. Missing generator → empty.
+    // GET /today — today's suggestions.
+    //   ?generate=0 → read-only (the panel's 30s poll uses this; never generates).
+    //   ?wait=1     → synchronous: block until generation finishes, then return
+    //                 the built items (the old default behavior; kept for callers
+    //                 that want the items in the same response).
+    //   default     → if empty, kick generation in the BACKGROUND and return the
+    //                 current items immediately with { generating:true }. This
+    //                 stops the endpoint from blocking for the multi-minute local
+    //                 LLM batch; the run-lock coalesces repeat hits onto one run.
     router.get('/today', async (req, res) => {
       const d = today();
       let items = store.load().filter(it => it.date === d);
+      const gen = loadGenerator();
       if (!items.length && req.query.generate !== '0') {
-        const gen = loadGenerator();
-        if (!gen) return res.json({ items: [], note: 'generator not built yet' });
-        try {
-          await gen.generateToday();
-          items = store.load().filter(it => it.date === d);
-        } catch (e) {
-          return res.status(500).json({ items: [], error: e.message });
+        if (!gen) return res.json({ items: [], generating: false, note: 'generator not built yet' });
+        if (req.query.wait === '1') {
+          try {
+            await gen.generateToday();
+            items = store.load().filter(it => it.date === d);
+          } catch (e) {
+            return res.status(500).json({ items: [], error: e.message });
+          }
+        } else {
+          gen.generateToday().catch(e => console.error('[engine] background generateToday failed —', e.message));
+          return res.json({ items, generating: true });
         }
       }
-      res.json({ items });
+      res.json({ items, generating: gen && typeof gen.isGenerating === 'function' ? gen.isGenerating() : false });
     });
 
     // POST /generate — force a generation run now (same lazy require).

← a30458a auto-data-snapshot: 2026-08-15T20:14:06 (1 data files) — pub  ·  back to Marketing Command Center  ·  auto-data-snapshot: 2026-08-16T03:49:55 (1 data files) — pub 6b14b13 →