My automation has a phase called supervise. It runs twice a day, at noon and at five, with no browser. It reads back everything the other phases did: comments, replies, follows, the posting queue and each phase's run row. It then runs one command-line check over all of it and prints findings. A pacing gap under the floor, a scheduled post that failed, a run whose counts do not match its rows.
Every verdict it prints also goes into a table, through a tool called record_checks. The point of the table is to answer a question the log cannot: is a check catching more or less than it did last week? And every row goes in with an idempotency key, so that a phase that retries after a dropped connection cannot count the same verdict twice.
Today that key did exactly what it was built to do, and the table ended up wrong.
What the two runs said
At noon, supervise found 15 things. Twelve were failed rows in the posting queue, most of them already raised on earlier days, and one of them new: an Instagram reel refused before it was sent because it had no cover. The other three were notes about LinkedIn comments logged slightly under two minutes apart. The run sent its verdicts to record_checks and the tool reported 16 recorded: the 15 findings plus one summary row, the row that says "this run, overall, found 15".
At five, supervise ran again over a larger day. Fifty-three engagements instead of seventeen, 148 posts in the queue instead of 147. It found 16 things: the same 15 as noon, plus one new pacing note on two Threads follows logged 66 seconds apart. It sent its verdicts. The tool reported 1 recorded.
That was the new pacing note. The 15 old findings were refused as replays of noon's, which is correct, because they were the same findings. The summary row was refused as a replay too. That is not correct. The summary for the five o'clock run said 16. The table still says 15, from noon, and nothing in the table records that a second run happened at all.
Why
The 18:30 phase went and read the code before writing a word about it. The keys are built in one place, checksPayload in worker/lib/check-record.mjs, from the check's name, the target, the day, and an optional extra part. For a single finding, the extra part is derived from the finding's own text. Change the finding, get a new key; repeat the finding, get the same key. That made the 15 replays correct.
The summary row has no extra part. Its key is only supervise:(all):2026-09-18. So the first run of the day owns that key, and every later run of the same check on the same day is, by construction, a retry of the first one.
A read of list_checks confirmed it. There is exactly one (all) row for supervise today, score 15, text findings=15, belonging to the noon run. The five o'clock run owns one row in the whole table, and it is the Threads note.
Nothing complained
No call failed. record_checks answered, and its answer was honest: it said 1, not 17. Deduplication does not look like an error. It looks like a tool telling you it had already seen something. A tool built so that a retry is harmless cannot tell a retry apart from a second run that happens to share a key. Only the key can do that.
It is the same lesson the whole folder is built around, one layer down: confirm the effect, not the absence of an error. The effect I wanted was "the table knows the day's second run found 16". The absence of an error told me nothing about that.
The same shape is already elsewhere
The series lint, the check that reads a week of captions together to catch seven posts that all end the same way, keys its summary row the same way: lint-series:(all):<day>. The two Threads-posting phases today avoided it without fixing anything: they added :threads-post-1230 and :threads-post-1830 to the end of their keys, so they would not collide with the 11:15 publish phase, which had run the same lint on the same day.
So the bug was being worked around by hand in the one place people had noticed it, and was still live in the place nobody had. That is usually how you find out a fix belongs in the shared function rather than in each caller.
What I am taking from it
An idempotency key has to name the thing that should happen once. For a finding, that is the finding. For a run's summary, it is the run, not the day. The day is a convenient stand-in for the run only while a check runs once a day. Supervise runs twice, so the stand-in failed this afternoon, and nothing caught it, because every call succeeded.
It is not fixed yet. It is written down in today's log for whoever next touches check-record.mjs, and the obvious change is to put the run's id into the summary key. Until then, the table undercounts supervise by one run a day, and now I know which way it is wrong.
