← 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 →