[object Object]

← back to Model Arena

fix(TK-12121): exit on EADDRINUSE (single-owner guard) + thumbnail timeout 20s->45s

8d2c52a5f390b1ec25d2f2f231afef16d6bcb25a · 2026-09-26 08:52:13 -0700 · Steve Abrams

The losing duplicate (pm2 vs launchd on :9758) stayed alive via the uncaughtException
handler and saved a stale challenges.json over the live server, erasing run.thumb/judged_at.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BAadfW5L9VDTcwyeVxhfB2

Files touched

Diff

commit 8d2c52a5f390b1ec25d2f2f231afef16d6bcb25a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Sep 26 08:52:13 2026 -0700

    fix(TK-12121): exit on EADDRINUSE (single-owner guard) + thumbnail timeout 20s->45s
    
    The losing duplicate (pm2 vs launchd on :9758) stayed alive via the uncaughtException
    handler and saved a stale challenges.json over the live server, erasing run.thumb/judged_at.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01BAadfW5L9VDTcwyeVxhfB2
---
 server.js | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/server.js b/server.js
index 5bb5752..724a5cd 100644
--- a/server.js
+++ b/server.js
@@ -35,7 +35,7 @@ function shootThumb(htmlPath, outPng, cb) {
   try { fs.unlinkSync(outPng); } catch {} // a stale png must never count as a fresh shot
   execFile(CHROME_BIN, ['--headless', '--disable-gpu', '--no-sandbox', '--hide-scrollbars',
     '--window-size=800,600', '--virtual-time-budget=2800', '--screenshot=' + outPng,
-    'file://' + htmlPath], { timeout: 20000 }, (err) => {
+    'file://' + htmlPath], { timeout: 45000 }, (err) => { // Chrome's teardown alone takes ~12s idle
       let ok = false; try { ok = fs.statSync(outPng).size > 0; } catch {}
       cb && cb(ok ? null : (err || new Error('no png')));
     });
@@ -1213,6 +1213,14 @@ iframe{width:100%;height:460px;border:0;background:#000}.c.win iframe{height:560
   res.writeHead(404); res.end('not found');
 });
 
+// TK-12121/TK-12270: a second owner (pm2 vs launchd) that loses the bind used to stay ALIVE —
+// the global uncaughtException handler swallowed EADDRINUSE — holding a stale in-memory copy of
+// challenges.json and saving it over the live server's writes (run.thumb / judged_at vanished, so
+// the daily battle never judged). A process that cannot own the port must exit, not linger.
+server.on('error', (e) => {
+  if (e && e.code === 'EADDRINUSE') { console.error('[model-arena] :' + PORT + ' already owned by another process — exiting (single-owner guard)'); process.exit(1); }
+  console.error('[model-arena] server error', e);
+});
 server.listen(PORT, function () {
   console.log('[model-arena] http://localhost:' + this.address().port + (BASIC_AUTH ? '  (basic auth on)' : '') + (isPaused() ? '  [CHALLENGES PAUSED — no model runs, no spend]' : ''));
   if (!isPaused() && resumeOnBoot.length) {

← b0b30ad auto-data-snapshot: 2026-09-26T08:50:16 (3 data files) — dat  ·  back to Model Arena  ·  auto-data-snapshot: 2026-09-26T09:21:27 (3 data files) — dat 4b88d13 →