← back to Wallco Ai
scripts/drunk_animals_daemon.js
91 lines
#!/usr/bin/env node
/**
* Drunk-animals daemon — runs as a pm2-managed long-lived process and fires
* scripts/drunk_animals_tick.js exactly every TICK_MS milliseconds. Replaces
* the launchd plist which got deferred from 4-min to 20-min cadence by
* macOS power-coalescing on Apple Silicon (idle system + StartInterval is
* "best effort", not guaranteed).
*
* Stops itself past STOP_HOUR PT (default 6 AM) — does not exit, but
* suppresses tick firings. Resumes the next evening if the daemon is still
* up (the tick script also has its own outside-window guard, redundant).
*/
const { spawn, execSync } = require('child_process');
const path = require('path');
const TICK_MS = parseInt(process.env.TICK_MS || '90000', 10); // 90s default (max-it mode)
const STOP_HOUR = parseInt(process.env.STOP_HOUR || '6', 10);
const ROOT = path.join(__dirname, '..');
const TICK_SCRIPT = path.join(__dirname, 'drunk_animals_tick.js');
// Preflight: spoon_all_designs_id_seq.last_value MUST be ≥ max(id), else
// every INSERT ... RETURNING id fails with a duplicate-key error and the
// generator silently drops designs (the 2026-05-13 05:25-05:50 PT incident
// where 6 generations went orphan). Self-heal by setval'ing past max(id).
function preflight() {
try {
const raw = execSync(
`psql dw_unified -At -F'|' -c "SELECT last_value, (SELECT max(id) FROM spoon_all_designs) FROM spoon_all_designs_id_seq;"`,
{ encoding: 'utf8' }
).trim();
const [seqStr, maxStr] = raw.split('|');
const seq = parseInt(seqStr, 10);
const max = parseInt(maxStr, 10);
if (!isFinite(seq) || !isFinite(max)) return;
if (seq < max) {
console.log(`[${new Date().toISOString()}] PREFLIGHT FIX: id_seq.last_value=${seq} < max(id)=${max} — self-healing`);
execSync(`psql dw_unified -At -c "SELECT setval('spoon_all_designs_id_seq', ${max + 1}, false);"`);
}
} catch (e) {
console.error(`[${new Date().toISOString()}] preflight err: ${e.message}`);
}
}
// Overnight gate removed 2026-05-13 — drunk-animals now fires 24/7 to
// match stoned-animals. Kept the STOP_HOUR / nowPT scaffolding inert
// so re-enabling is one diff if Steve wants the gate back.
function inWindow() { return true; }
let running = false;
function fire() {
if (running) {
console.log(`[${new Date().toISOString()}] previous tick still running, skipping`);
return;
}
if (!inWindow()) {
const h = nowPT().getHours();
if (h % 4 === 0) console.log(`[${new Date().toISOString()}] outside window (PT hour ${h}), idle`);
return;
}
running = true;
preflight();
const t0 = Date.now();
const child = spawn('node', [TICK_SCRIPT], { stdio: 'inherit', cwd: ROOT });
// Hard timeout — last night a Replicate call hung for 16 min, blocking
// 3+ subsequent ticks. Cap at 5 min so the next 4-min tick can fire on
// time even if Replicate is sick.
const killTimer = setTimeout(() => {
if (!child.killed) {
console.error(`[${new Date().toISOString()}] tick exceeded 5min — sending SIGKILL`);
try { child.kill('SIGKILL'); } catch {}
}
}, 5 * 60_000);
child.on('exit', code => {
clearTimeout(killTimer);
running = false;
console.log(`[${new Date().toISOString()}] tick done in ${((Date.now()-t0)/1000).toFixed(1)}s (exit ${code})`);
});
child.on('error', err => {
clearTimeout(killTimer);
running = false;
console.error(`[${new Date().toISOString()}] tick spawn error:`, err.message);
});
}
console.log(`[${new Date().toISOString()}] drunk-animals daemon up · TICK_MS=${TICK_MS} STOP_HOUR=${STOP_HOUR}PT`);
console.log(`[${new Date().toISOString()}] first tick in ${TICK_MS/1000}s`);
setInterval(fire, TICK_MS);
// Don't fire on boot — give pm2 a moment, then ride the setInterval cadence