fix(coach/trigger-tuner): crash after ~1h — runOnce is sync, not a Promise
The live bot died overnight with:
TypeError: runOnce(...).catch is not a function
at trigger-tuner.js:42 → [supervisor] child exited code=1
attach() wrapped the timer body as `runOnce().catch(...)` but
runOnce() returns a plain {ok, flagged, ...} object (pure SQL, no
await). The first tuner tick (60min after spawn) threw → killed the
whole bot process. Never surfaced before because the bot rarely ran
uninterrupted for a full hour during development.
Fix: guard the synchronous call with try/catch, matching how
persona/chatter.js already does its sync tick. (postmortem.drainOnce
and reflect.runOnce ARE async, so their .catch is correct — audited.)
Regression test added: captures the setInterval callback and invokes
it synchronously, asserting it does not throw.
Tests: 397 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -6,10 +6,31 @@ import { join } from "node:path";
|
||||
|
||||
import { initKnowledge, isAvailable, listImprovements } from "../knowledge/index.js";
|
||||
import { closeStore, __resetForTests } from "../knowledge/store.js";
|
||||
import { runOnce, __testing } from "./trigger-tuner.js";
|
||||
import { runOnce, attach, detach, __testing } from "./trigger-tuner.js";
|
||||
|
||||
const { MIN_SAMPLE } = __testing;
|
||||
|
||||
test("attach: timer tick does not crash (runOnce is sync, regression for .catch bug)", async () => {
|
||||
// Reproduces the crash that killed the live bot after ~1h: the
|
||||
// setInterval body called runOnce().catch(...) but runOnce returns
|
||||
// a plain object, not a Promise. attach must guard with try/catch.
|
||||
detach();
|
||||
let threw = false;
|
||||
const origSetInterval = globalThis.setInterval;
|
||||
let captured = null;
|
||||
// capture the interval callback without actually waiting
|
||||
globalThis.setInterval = (fn) => { captured = fn; return { unref() {} }; };
|
||||
try {
|
||||
attach({ intervalMs: 999999 });
|
||||
// invoke the captured tick synchronously — must not throw
|
||||
try { captured?.(); } catch { threw = true; }
|
||||
} finally {
|
||||
globalThis.setInterval = origSetInterval;
|
||||
detach();
|
||||
}
|
||||
assert.equal(threw, false, "tuner timer tick must not throw");
|
||||
});
|
||||
|
||||
async function bootstrap() {
|
||||
const tmp = mkdtempSync(join(tmpdir(), "pepa-tuner-test-"));
|
||||
__resetForTests();
|
||||
|
||||
Reference in New Issue
Block a user