My AI rewrote the same client summaries 2,575 times in two weeks. I didn't ask for a single one of them.
Nothing crashed, every check was green, and the summaries even read well. The only sign was a number I wasn't measuring.
The setup
My command center keeps an AI-written summary on every client and project. A background heartbeat refreshes them when something changes, so a summary is never older than the facts it describes.
The generator writes each summary into the client's markdown file. A file watcher marks any changed markdown as stale, so the heartbeat regenerates it. Read those two sentences again: that's the bug.
- 1The AI writes a summary
- 2The watcher sees a changed file
- 3It marks the summary stale
- 4The heartbeat regenerates it
The heartbeat even had an anti-loop rule: a regeneration cleared its own marker and never nudged the scheduler. But the watcher was a second door, it couldn't tell the AI's write from mine, and every generation re-armed its own trigger. Each client re-queued itself every three minutes for as long as the app was open.
0
summary regenerations in 14 days
peak: 532 in a single day
What it cost
- The model calls. A client summary costs about $0.42, and a forced pass over all 42 AI reads in my system about $19. Priced per call, the loop was worth on the order of $1,000 every two weeks, spent re-saying the same things.
- The absurd part. One churned account, dead since June, was re-narrated 144 times. On one day, five versions of its summary landed, all saying the same thing in different words.
- The hidden part. My background AI makes at most two calls per pass, and the loop took almost all of them. Over 40 consecutive passes, the scheduler picked a summary 63 times, the morning brief 16 times and the weekly review once.
- The jobs that never ran. In the same two weeks: 2 company-level reads and 0 evals. Everything queued below the summaries was unreachable by construction, and nothing reported it as a failure, because nothing had failed.
That last one stings. The daily self-check waited for an empty queue that never came, starved for 15 days by the very problem it existed to find. When the loop closed, it ran again, along with the company reads, the weekly review and summaries of twelve people in my network that had never been written at all.
Why nobody noticed
Freshness was decided by a clock: a dirty marker plus a seven-day age-out. A clock is a proxy for "something changed", and this one was wrong in both directions at once.
Before
Regenerate on a clock
- Too loud: re-narrates an account whose facts can't change
- Too quiet: misses a task marked done until the timer fires
- Counts its own writes as changes
After
Regenerate on a change
- A fingerprint of exactly what the read is built from
- Unchanged inputs: no model call at all
- Changed inputs: regenerate now, whatever the age
- Its own writes are not changes
- In doubt, it regenerates: a wasted call beats a stale read
Every version read fine on its own, so you'd only see the loop by counting, and nothing counted. Once I did, every dirty marker turned out to be stamped one second after its own last run, with the reason "external edit". The AI was the external editor.
The fix: a fingerprint of the inputs
Every AI read now carries a fingerprint of the sources it's built from: the client's file without the AI-written fields, its linked tasks with their status, the journal entries that mention it, its pipeline rows.
Same fingerprint, same sources, so the model isn't called. A different fingerprint means regenerate now: a summary whose headline names a task refreshes the moment that task is marked done, and an unrelated client moving doesn't touch it.
Three rules for any AI that writes what it reads
- 1The AI's own write is not a change. The app now records every write it makes. The watcher still picks those files up, but never marks them stale.
- 2"Updated" means the content moved, not that the AI ran. A regeneration that produces the same text writes nothing, because a timestamp that moves on every run tells you nothing.
- 3A clock is only a backstop. The age-out survived, keyed by status, so a churned account is no longer re-narrated like a live retainer.
Measure the calls that changed nothing
Here's what I'd do differently from day one. The CLI reported usage and cost on every call, and my code parsed both and threw them away. The numbers that would have shown the loop on day one were in my hands the whole time.
Now every model call is logged with its tokens, its cost and one more field: did it change anything? The share of calls that changed nothing is the health metric. Near zero is healthy; a climbing share means something is regenerating on a timer again.
A regeneration loop looks like nothing from the inside. From the outside, it's a share of calls that changed nothing.
What I tell anyone running AI in the background
If your AI refreshes on a timer, it isn't watching your data. It's watching the clock.
Weekly-ishEngineering OS, in your inboxJoin Engineering OS


