Beagle SCM
JS-113: a throwing timer callback permanently desyncs the JS timer wheel
Proposed by the 2026-07-07 jab multi-agent review; confirmed by both skeptic verifiers. After one callback throw, the native POL timer self-removes while the JS wheel still believes it is armed — every later setTimeout/setInterval never fires.
Input
Context
- A throw inside twTick (jab net.cpp:374-387) escapes pol._timer, so JABCPolCall returns -1 and JABCPolTimer returns 3600000 (pol.cpp:67-70), self-removing the native timer; JS-side
twOn stays true and timer stays set.
- Subsequent g.setTimeout -> twArm (net.cpp:388) sees twOn===true and only calls pol.sooner() — a no-op with no native timer armed ("POLNONE fine", pol.cpp:120) — so no timer ever fires again.
- Scenario:
try { pol.run() } catch(e) {}; setTimeout(fn, 10); pol.run() hangs forever.
Goals
- Repro first: the hang scenario as a (timeout-guarded) test.
- Re-arm robustly: twArm always re-registers via pol.timer (or twTick catches, reports, and keeps the wheel consistent).
Constraints
- Callback exceptions must still surface (Node reports uncaught timer throws); the wheel just must survive them.
WIP
TODOs