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:
2026-05-28 07:22:44 +03:00
co-authored by Claude Opus 4.7
parent 08ac2e1bc1
commit b68d4b3ee7
2 changed files with 31 additions and 2 deletions
+22 -1
View File
@@ -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();