{"slug": "an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead", "title": "An AI agent graded its own poll loop as 'event-driven'. I read the code instead.", "summary": "A developer found that an AI agent's self-described 'event-driven' monitor was actually a 90-second poll loop, not event-driven. The agent had conflated 'does not re-arm itself every turn' with 'does not poll,' leading to up to 90 seconds of latency and about 960 polls per day. The developer, FreddieMcHeart, used this as a lesson to refactor downbeat's watch command to use a proper event-driven watcher that was already in the codebase.", "body_md": "`downbeat`\n\n(my tool that passes messages between terminal sessions, [github.com/FreddieMcHeart/downbeat](https://github.com/FreddieMcHeart/downbeat)) has two ways to keep a session aware of its inbox. One is `/relay-monitor`\n\n: it composes the built-in `/loop`\n\n(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.\"\n\nI 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.\n\nThe self-described \"event-driven\" monitor was this:\n\n```\nwhile true; do\n  sleep 90\n  cur=$(downbeat inbox | grep '^*' | awk '{print $2}' | sort)\n  new=$(comm -13 \"$seen\" <(printf '%s\\n' \"$cur\"))\n  [ -n \"$new\" ] && { echo \"NEW MESSAGES\"; printf '%s\\n' \"$cur\" > \"$seen\"; }\ndone\n```\n\nThat 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.\n\nReading past the wrong label, two ideas in it were genuinely good.\n\nFirst, a cheap gate in front of an expensive consumer. On a quiet channel, `/relay-monitor`\n\nspends 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`\n\nactually 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.)\n\nSecond, 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.\n\nHere is where it turned into a lesson about my own codebase. `downbeat`\n\nalready had a real event-driven watcher: `src/downbeat/core/watcher.py`\n\n, with an `FsWatcher`\n\nbuilt on watchdog (FSEvents on macOS, inotify on Linux) and a `PollWatcher`\n\nfallback for network filesystems, both behind a `make_watcher()`\n\nfactory. I had written it months earlier for the TUI's live refresh, and it was sitting unused.\n\nMeanwhile the `downbeat watch`\n\ncommand I had added more recently (since removed, see the update below) ignored it and rolled its own `while sleep`\n\nloop.\n\nSo 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.\n\nRewire the watch command to use the factory:\n\n```\nw = make_watcher(on_change=emit, prefer=\"auto\")   # FsWatcher, PollWatcher fallback\n# --poll forces PollWatcher for NFS/SMB, where FS events are unreliable\n```\n\nNow `downbeat watch`\n\nblocks 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.\n\nThe 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.\n\nSince I wrote this, that `FsWatcher`\n\ngot promoted again. It made the standalone `watch`\n\ncommand this post celebrates redundant. Instead of a user running `watch`\n\nin 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`\n\nsubcommand was removed outright.\n\nWhich 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.", "url": "https://wpnews.pro/news/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead", "canonical_source": "https://dev.to/nazarii-ahapevych/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead-149i", "published_at": "2026-07-20 16:40:23+00:00", "updated_at": "2026-07-20 17:03:57.183862+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["downbeat", "FreddieMcHeart"], "alternates": {"html": "https://wpnews.pro/news/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead", "markdown": "https://wpnews.pro/news/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead.md", "text": "https://wpnews.pro/news/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead.txt", "jsonld": "https://wpnews.pro/news/an-ai-agent-graded-its-own-poll-loop-as-event-driven-i-read-the-code-instead.jsonld"}}