← Back to lab

Three Nights of Byte-Identical Audit Reports. The Fix Queue Never Got a Turn.

Nightly agent audits re-ran byte-identical for three nights while eight staged fixes starved behind them. Anatomy of a report loop that never executes.

Three Nights of Byte-Identical Audit Reports. The Fix Queue Never Got a Turn.
Queue:     31 files on the night of 2026-07-10 — 23 standing audits + 8 staged fixes
Executed:  usage-limit wall at slot 18  |  Fix tasks that ran: 0
Evidence:  reports 13 and 14 byte-identical, 3 consecutive nights

The Symptom

Reports 13 and 14 of my nightly audit run came out byte-identical three nights in a row. Same findings, same severity labels, same "cheapest fix first" ranking. cmp returned zero differing bytes. An audit loop that regenerates its output verbatim every night is not auditing — it's rehearsing.

The loop runs headless overnight: markdown task files with model and working-dir headers, executed by a CLI runner in print mode, Telegram notification on pause, fail, and complete. Nobody watches it live. The reports are the only product, and nothing downstream was checking whether they ever changed.

A Sort Order Starved the Fixes

The runner picks the next task like this:

# Lexicographic. Standing audits are numbered 01–49,
# one-shot fix tasks are staged as 50+. Audits sort first. Every night.
ls queue/*.md | sort | head -1

On 2026-07-10 the queue held 31 files. The eight revenue fix tasks sat at slots 24–31. The provider usage-limit wall hit at slot 18. The rate-limit handler sleeps 30 minutes and retries the same task for up to 12 hours — then the night ends. Zero fix tasks ran.

The next night, the audits re-discovered the same unfixed findings at full token cost, and reports 13 and 14 regenerated them verbatim. The loop was paying to re-learn its own backlog.

DONE Means the Process Exited

Diagnosing the starvation meant reading the runner log against report file mtimes. Tasks 04, 12, and 16 regularly logged DONE with 450–2,200 bytes of output while their report files kept a date three days older. On 2026-07-10, task 12 logged DONE at 00:27; its report was dated three days prior.

DONE records that the process exited, not that it wrote anything. It is the same class of lie as an exit code 0 on work that never happened — and in a loop nobody watches, it compounds nightly.

The Summary Ran Before Half the Reports Existed

The executive-summary task is numbered 11. Reports 12 through 23 sort after it. The summary was therefore always built from the previous night's content, by construction of the sort order.

On 2026-07-20, reports 01–07 were from the previous night and only 08–10 were fresh. Splitting a run across nights had become routine — the quota wall ends the night wherever it lands — and the summary dutifully synthesized whatever mix of ages it woke up to, with no age labels anywhere in its output.

Wrong Probes, Promoted to CRITICAL

Two headline criticals on 2026-07-20 were wrong:

  • A report declared an image-generation API token dead. It had probed GET /v1/user, an endpoint that does not exist in that API. The real account endpoint returned 200.
  • A report declared zero tasks routed to the cheap model without reading the provider field in the task store. Five tasks carried it.

Same failure shape both times: a wrong probe read as evidence of breakage, then promoted to CRITICAL because the probe failed loudly. That night the reports also undercounted live state — 48 hung feed runs reported against 54 actual, 5 missing scheduler tasks against 9 — and a "crash loop, 16 restarts" finding went stale within hours because PM2 restart counters reset on reboot. Related trap: the runner flushes PM2 logs nightly around 23:01, so any audit reading error logs sees less than 24 hours of history and keeps concluding "no errors."

The Dangerous One: Stale Advice With an rm Block

One standing report owns stale-project triage: list what looks dead, recommend archival. Across three consecutive synthesis runs — each verified against live systems — it recommended archiving five services that are all alive: a deliverability-testing worker, a screenshot service, a background-removal tool, a catalog app, and a docs site. The recommendation ships with a ready-to-run rm block.

The triage report was not hallucinating. Its evidence was real — a static inventory snapshot — but liveness on this box lives in cron configs, a second scheduler, and PM2 process state the report never reads. Its conclusion was wrong three weeks running, confidently, with destruction attached.

The rule that came out of it: nothing generated by an unattended audit ever executes a delete. An audit proposes; a human or a separately reviewed script disposes.

The Loop Never Closed

Four more findings from the same anatomy, in descending order of embarrassment:

  • 0 of 7 moves from a growth plan were executed. The fix-staging script only parsed the executive summary — which had since been rewritten for other purposes. Nobody counted applied diffs, so nobody noticed for a week.
  • 12 fix tasks staged in early October were still unapproved a week later, while the audits re-found them nightly. The loop's real output was a growing list of things it already knew.
  • The one undated executive summary from mid-August was overwritten with no copy. Lost.
  • The runner's cheap-model branch turned out to be a placeholder that logs "not installed" and silently invokes the expensive default. The label said one model; the invoice said another.

What the Synthesis Step Does Now

# Before synthesizing anything:
stat -c '%y %n' reports/*.md | sort        # age every report first, label it in output
cp EXECUTIVE-SUMMARY.md EXECUTIVE-SUMMARY-$(date +%F).md   # dated copy BEFORE overwrite
cmp reports/13-*.md previous/13-*.md       # byte-diff vs last run — identical means dead night

Every summary since carries dated copies, per-report age labels, and re-probes of the top findings against live systems before anything gets ranked. A critical stays unverified until its probe is confirmed to hit an endpoint that exists.

SymptomRoot causeDetector
Byte-identical report, 3 nightsFixes starved behind auditscmp against previous run
DONE logged, report 3 days oldRunner logs exit, not outputReport mtime vs run timestamp
Summary built from stale dataSummary task sorts before half the reportsAge label on every report
Dead-token CRITICALProbe hit a nonexistent endpointRe-probe before ranking
Archive advice on live servicesStatic inventory, live liveness elsewhereManual probe; no generated rm
0 of 7 moves executedStager parsed the wrong fileCount applied diffs per week

What I'd Change

The picker still runs lexicographic. The proposed fix — one-shot fix tasks sorting ahead of standing audits — is staged, not applied, and this post is the argument for applying it. Beyond that: DONE should mean "report mtime updated," not "process exited," and every standing report should open with what changed since the previous run. A report whose delta section is empty three nights running has told you everything you need to know about the loop it lives in.