My Briefing Said Zero for a Week
7 min read
Every morning my homelab sends me a briefing. One of its lines reads like this:
Articles: 154 fetched -> 54 curated -> 0 ingested
I'd been reading that line for a week. A hundred and fifty-four articles pulled from thirty-nine sources, curated down to fifty-four by a local model, and then nothing. Zero ingested into memory. I had a theory about it, too — my curation prompt is deliberately harsh, it's instructed that "most should score 3-5," and the ingest gate is a score of 5. Tight gate, nothing gets through. Made sense. Slightly annoying, clearly working as designed.
It was not working as designed. The run behind that exact line ingested thirty-two articles and generated twelve improvement proposals. The briefing said zero — and since July 9th it could not have said anything else, on any night, no matter what the pipeline did.
The number was a literal
On July 9th I split the article pipeline. Deep-ingest took about twenty-two minutes and proposal generation about eighteen, and both were sitting on the critical path of the morning briefing — which meant my briefing was forty minutes late because it was waiting for work it didn't need to report. So I moved both phases into a separate downstream step, article-proposals, and let the scanner finish fast.
The scanner's return value picked up two new lines:
"articles_ingested": 0, # deferred to the article-proposals step
"proposals_generated": 0, # deferred to the article-proposals step
That comment is correct. It is, in fact, an unusually good comment. It says precisely what happened and why the zero is there.
And then the briefing kept reading articles_ingested from the scanner.
No code path anywhere read the article-proposals result. Not one. So the line wasn't reporting a low number, or a stale number, or a number from a bad night. It was reporting a constant. On a night that ingested three hundred articles it would have printed zero. The value had been severed from the thing it claimed to measure, and the severing was documented, in-line, three feet away from the code that trusted it.
Why I believed it
Here's the part I keep turning over. A zero is not an alarm. A zero is a number.
If that line had said ERROR or unknown or —, I'd have looked into it in about ninety seconds. Instead it said 0, and 0 is plausible. I had a whole story ready for why it was zero: the strict prompt, the tight gate. My explanation was good. I'd have defended it. I'd have told you about the hard filter and the "be selective" instruction and how a 5-of-10 threshold is aggressive on purpose.
I was explaining a constant. All that reasoning was fitted to a value that had no relationship to the world. That's what a fabricated number does that a missing one doesn't — it recruits you. It gives your pattern-matching something to chew on, and you supply the causal story for free.
This is the fourth time I've shipped this same bug in about ten days, wearing four different costumes.
On July 6th, headless Claude's OAuth died machine-wide, and my overnight chain scored every one of those tasks COMPLETED — fourteen-second 401 no-ops, all green, nothing woke me. On the 16th, the chain briefing reported "17 of 17 steps complete" because the denominator was computed from the numerator's own source; the ratio was 100% by construction, and eleven of twenty-eight steps hadn't run at all. Later that same day, the CI reviewing that very fix hit a 102KB diff, truncated it at 60KB, reviewed the first 58%, and reported a pass. And underneath all of it, the briefing had been printing a literal zero every morning since the 9th.
Different code every time, same shape every time: a number that reads as observed but is manufactured by construction. Not a bug class I can type-check my way out of. It's an epistemics problem wearing a syntax costume.
The pattern in the dates is the part I can't shrug off. Four instances in ten days is not four accidents. It's a systematic bias in how I build: I wire up the thing that produces the number and treat the number as a free consequence, when it's a separate claim that needs its own evidence.
The fix, and the part I'd already written
The repair is small. Read the count from the step that does the work:
Articles: 154 fetched -> 54 curated -> 32 ingested
The interesting bit is what happens when the answer isn't available. article-proposals runs off the briefing's critical path — that was the entire point of the split — so on some cycles it hasn't reported yet when the briefing is built. The old code would have printed zero there too. The new code prints what it actually knows:
Articles: 154 fetched -> 54 curated -> 36 ingest-eligible, ingest pending
And when it doesn't even know that: ingest unknown. An absent step is unknown, not zero. That's the rule the endpoint under my dashboard already runs on, and it should have been the rule here.
The genuinely humbling detail is where that 36 came from. I didn't add it tonight. It's in the scanner's result payload already, produced by a helper whose docstring says it returns the count "for observability."
And it shipped in the same commit as the bug. The diff that hardcoded articles_ingested: 0 also added ingest_eligible three lines below it — the honest number and the fake one, born together, in one commit, by me, in one sitting. I wrote the zero, then immediately wrote the thing that would have exposed the zero, gave it a docstring announcing its purpose, and wired it to nothing.
That's not carelessness. That's something stranger. Some part of me knew the deferred count needed a replacement — enough to compute one and label it "for observability" — and no part of me connected that to the line that was about to start lying. I did the hard half and skipped the trivial half, and then read the result every morning for a week without the two facts ever touching.
I didn't lack the data. I had the data, a comment explaining the zero, and a helper built specifically to expose the real figure. What I lacked was any mechanism that would notice the number I was reading had come unhooked from the number I meant.
Proving it on the actual night
I have a rule about this now, because I've broken it enough times to need one: a future scheduled run is not a verification plan. If I'd shipped this fix and said "we'll see at 5 AM," I'd have converted a two-minute check into a nineteen-hour wait and handed the discovery to a cron job.
So I pulled today's real result payloads out of live history and fed them through the new code:
scanner result: articles_ingested=0, ingest_eligible=36 (19:24:08Z)
proposals result: articles_ingested=32, proposals_generated=12 (19:50:04Z)
DEPLOYED CODE, LIVE COLLECTOR -> Articles: 154 fetched -> 54 curated -> 32 ingested
Thirty-two, from the exact run the briefing scored as zero. Deployed, restarted, and proven against production data before I wrote this sentence.
One honest limit on the week. My task history holds two hundred entries and reaches back about a day, so I can't show you seven mornings of zeros — I have receipts for one cycle. The "every night since July 9th" claim is an argument from the code, not a row I can point at: the value was a literal, so no night could have printed anything else. That's a strong argument and it isn't the same thing as evidence, and after the day I've had, I'd rather label the difference than let it ride.
What I can show you is that on the one cycle I can inspect, the pipeline fetched a hundred and fifty-four articles, curated fifty-four, ingested thirty-two, and wrote twelve proposals — while the only thing I ever read about it said it had done nothing.
A dashboard that can't say unknown will say zero instead. And you'll believe it, because zero is a number, and numbers feel like observations.