← Back to PRs

#22340: fix(heartbeat): drain system events after event-driven heartbeat run

by AIflow-Labs open 2026-02-21 02:06 View on GitHub →
agents size: M
## What changed - In `runHeartbeatOnce`, drain queued system events when an event-driven heartbeat run is processed (`exec-event`, `cron`, `wake`, `hook`, or tagged cron), instead of only peeking. - Add a regression test to ensure an exec completion event is consumed on the first event-driven heartbeat and does not re-assert on the next event wake. ## Why this fixes the issue `issue #22320` reports duplicate heartbeats shortly after tool completion. Heartbeat wakeups were using `peekSystemEventEntries` and did not clear consumed events, so repeated wake triggers could reprocess the same completion context and produce clustered follow-up heartbeats. Draining the queue for event-driven runs makes each tool-completion event one-shot. ## Tests - `pnpm vitest run --config vitest.unit.config.ts src/infra/heartbeat-runner.returns-default-unset.test.ts src/infra/heartbeat-runner.ghost-reminder.test.ts` - `pnpm test -- src/infra/heartbeat-runner.returns-default-unset.test.ts` - `pnpm vitest run --config vitest.unit.config.ts src/infra/heartbeat-wake.test.ts src/infra/heartbeat-runner.scheduler.test.ts` ## Edge cases - Non-event-driven heartbeats (interval) are unaffected. - Event-driven failures continue to be retried per existing wake/scheduling behavior. - `exec-event` events are now one-shot and consumed only after processing. ## Known infra flake / blockers - None observed in local test runs. <!-- greptile_comment --> <h3>Greptile Summary</h3> Fixes duplicate heartbeat issue by draining system event queue after event-driven runs and adds Google Gemini 3.1 Pro Preview model fallback. **Heartbeat fix (commit bb97948a):** - Changed from `peekSystemEventEntries` to `drainSystemEventEntries` for event-driven heartbeats (`exec-event`, `cron`, `wake`, `hook`) - Added `withDrainedPendingEvents` wrapper to ensure events are cleared only for event-driven runs - Added regression test verifying exec completion events are consumed once **Model catalog fix (commit c9038111):** - Added `applyGoogle31ProPreviewFallback` to inject `gemini-3.1-pro-preview` when missing from catalog - Updated test from `toEqual` to `toContainEqual` to handle injected fallback models **Issues found:** - Line 878 in `heartbeat-runner.ts` missing `withDrainedPendingEvents` wrapper (one return path after LLM call doesn't drain events) <h3>Confidence Score: 3/5</h3> - PR addresses reported issue but has one missed drain path that could reintroduce bug in edge case - Core logic correctly switches from peek to drain and wraps most return paths, but line 878 (`alerts-disabled` after LLM call) bypasses the drain wrapper, potentially leaving events queued in that edge case. Model catalog changes are straightforward and properly tested. - src/infra/heartbeat-runner.ts line 878 needs drain wrapper added <sub>Last reviewed commit: bb97948</sub> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs