fix(ralph): loop stops rescheduling after two ticks

The reschedule guard is `this._status === 'running' && this.loopTimer === null`,
but the timer callback never nulls `loopTimer`. So the handle stays non-null from
the first fire onward, the guard is false on every subsequent pass, and the Ralph
loop silently stops polling after exactly two ticks.

It stops without changing status: `status` stays `running`, `stop()` is never
called, and no error is raised — the loop just quietly never runs again, which is
what makes it hard to notice on a long autonomous run.

Null the handle inside the callback before re-entering `runLoop()`, which is the
pattern `orchestrator-loop.ts` already uses for its own reschedule.

Test: a regression case in test/ralph-loop.test.ts that runs a real 5ms-interval
loop for ~16 intervals and asserts it ticks at least 3 times. Against the unfixed
source it reports exactly 2.
This commit is contained in:
Aamer Akhter
2026-08-20 12:58:17 -04:00
parent 07b9c7fd7b
commit a35438ba34
2 changed files with 39 additions and 6 deletions
+8 -1
View File
@@ -281,7 +281,14 @@ export class RalphLoop extends EventEmitter {
// Guard: only reschedule if still running AND no timer is pending
// (prevents race where stop() clears timer between our check and setTimeout)
if (this._status === 'running' && this.loopTimer === null) {
this.loopTimer = setTimeout(() => this.runLoop(), this.pollIntervalMs);
// Null the handle when the timer fires, BEFORE re-entering runLoop —
// otherwise the `loopTimer === null` guard above stays false on the
// next pass and the loop stops rescheduling after 2 ticks.
// Mirrors the orchestrator-loop reschedule pattern.
this.loopTimer = setTimeout(() => {
this.loopTimer = null;
this.runLoop();
}, this.pollIntervalMs);
}
});
}