An AI agent graded its own poll loop as 'event-driven'. I read the code instead.

작성자

카테고리:

← 피드로
DEV Community · Nazarii Ahapevych · 2026-07-21 개발(SW)

Context

downbeat (my tool that passes messages between terminal sessions, github.com/FreddieMcHeart/downbeat) has two ways to keep a session aware of its inbox. One is /relay-monitor: it composes the built-in /loop (re-fire a prompt every N minutes) with the inbox hook, so every tick is a full model turn whether or not new mail arrived. The other showed up on its own. A different session had set up a background “Monitor” and, in its own notes, described it as “event-driven, instant, cheap, only fires on events.”

I was asked which approach was better. We already had a rule for evaluating external tools: do not trust the description, read the code. I pointed that rule at the session’s own work.

The finding

The self-described “event-driven” monitor was this:

while true; do
  sleep 90
  cur=$(downbeat inbox | grep '^*' | awk '{print $2}' | sort)
  new=$(comm -13 "$seen" <(printf '%s\n' "$cur"))
  [ -n "$new" ] && { echo "NEW MESSAGES"; printf '%s\n' "$cur" > "$seen"; }
done

Enter fullscreen mode Exit fullscreen mode

That is a poll loop. It sleeps 90 seconds, lists the inbox, and diffs against a seen-file. The session had conflated “does not re-arm itself every turn” with “does not poll.” It graded itself on the label, not the loop. Its own comparison table claimed instant latency and cost only on events. Reality: up to 90 seconds of latency and about 960 polls a day, traffic or no traffic.

Two things it got right anyway

Reading past the wrong label, two ideas in it were genuinely good.

First, a cheap gate in front of an expensive consumer. On a quiet channel, /relay-monitor spends a full model turn every interval just to read “nothing new.” This script’s tick is a bash poll, near-free, and it only escalates to a model turn when comm actually finds new mail. Put the cheap check on the hot path and the expensive one on the rare path. That is the real win, whatever you call it. (Exact cost delta: unmeasured. The direction is obvious, I did not benchmark the size.)

Second, persistent state beats re-creation. Its previous version had re-armed a fresh “fire once” watcher every turn, which stacked up overlapping watchers that double-fired. One long-lived process with a seen-set emits each message exactly once. It had quietly re-derived a consumer offset in bash.

The kicker

Here is where it turned into a lesson about my own codebase. downbeat already had a real event-driven watcher: src/downbeat/core/watcher.py, with an FsWatcher built on watchdog (FSEvents on macOS, inotify on Linux) and a PollWatcher fallback for network filesystems, both behind a make_watcher() factory. I had written it months earlier for the TUI’s live refresh, and it was sitting unused.

Meanwhile the downbeat watch command I had added more recently (since removed, see the update below) ignored it and rolled its own while sleep loop.

So two independent agents, that other session and my own past self, each re-derived “poll the inbox in a loop” while a tested, event-driven watcher sat one import away.

The fix

Rewire the watch command to use the factory:

w = make_watcher(on_change=emit, prefer="auto")   # FsWatcher, PollWatcher fallback
# --poll forces PollWatcher for NFS/SMB, where FS events are unreliable

Enter fullscreen mode Exit fullscreen mode

Now downbeat watch blocks on filesystem events with near-zero idle cost, and running it under a monitor gives exactly the cheap-gate-then-wake architecture the other session was reaching for. The difference is that the “event-driven” claim is finally true instead of aspirational.

What I took away

  • Grade a mechanism by its code, not its description. Even when the description is your own, and especially when an AI wrote it. The gap between “what I built” and “what I said I built” is exactly where this bug lived.
  • Before building a new primitive, check whether you already shipped one. A ten-second grep of my own repo would have prevented two separate reinventions of “poll the inbox.”
  • Cheap predicate, then expensive work. A near-free filesystem check in front of an LLM turn is the pattern that actually mattered here, and it shows up all over the codebase once you go looking for it.

The most valuable output of the whole exercise was not the feature. It was noticing that a tested primitive had been re-derived twice, in plain sight.

Update: the primitive found a better home

Since I wrote this, that FsWatcher got promoted again. It made the standalone watch command this post celebrates redundant. Instead of a user running watch in a loop, the watcher now lives inside the TUI: while the app is open it fires a native OS notification the moment a peer has mail waiting and that peer has been idle too long, with a heartbeat so a headless send-hook and the TUI don’t both notify for the same message. The standalone watch subcommand was removed outright.

Which is this article’s own lesson, one turn further. The tested primitive I found sitting unused didn’t just replace two hand-rolled poll loops. Once it was the obvious building block, the right place to use it turned out not to be the command I first bolted it onto at all.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다