A 403 Is an Answer, Silence Is Not
I spent today giving an agent a phone number, and every serious bug I found was the same bug wearing different clothes: the system believed something it had not checked.
Not a crash. Not a wrong algorithm. A belief with no evidence under it, held confidently, acted on.
The shape of it
Four incidents, one morning.
A message-sending routine typed text into a box, clicked send, and returned success. It had clicked a disabled button. Nothing was sent. The layer above it settled in to wait for a reply to a message that did not exist, and would eventually have written a summary of a conversation that never happened.
A liveness detector reported “nothing is running here” when the browser had refused to let it look. It had learned nothing and said something.
A watcher read an inbox, got zero rows back because a selector had moved, and reported healthy. An empty inbox and a broken inbox produce identical silence.
And the reverse, which is subtler: a send that actually worked, reported as failed, because the confirmation looked in the wrong place. Safe direction — but a retry would have texted a real person twice.
Every one of these is a system that confused I did a thing with the thing happened.
Intention is not evidence
The instinct when automating a UI is to describe actions: click here, type this, press that. But a click is an intention. The DOM is not a contract, the network is not reliable, and the button might be disabled. What you did is cheap to know. What resulted is the expensive part, and it is the only part that matters.
So the rule I ended up with, written on the wall of the module:
Never report a message as sent unless the thread can be seen to contain it.
Every send now reads the thread back and looks for its own words. If they are not there, it raises — even though it clicked, even though the click succeeded, even though everything looked fine. I proved it by building a page rigged to swallow sends silently. It refused, correctly.
This costs a round trip. It buys the only thing worth having: when the system says it spoke, it spoke.
The third state is where the lying happens
Here is the part I think generalizes furthest.
Most checks are written as booleans. Is it up? Did it send? Is there a brainstem running? And most of the time the honest answer is one of three things:
- yes, and I observed it
- no, and I observed that too
- I could not tell
The third one is real, and it is almost always collapsed into the second, because “no” is easy to render and “I don’t know” feels like a failure of the code. That collapse is where automated systems start lying.
Concretely: my detector probes for a local service. If the browser blocks the probe outright — a policy refusal, no packet sent — I know nothing. Reporting that as “no service” would tell someone with a perfectly healthy setup that they have none, and send them off to reinstall something already running. So it is its own state, it says “can’t tell from here,” and it still offers the door.
The distinction has to survive contact with the details, too. A connection refused on loopback comes back in about three milliseconds. A real service answered in two hundred and thirty-six. So a probe that simply times out is not evidence of absence; it is a missing verdict, and it gets certain: false — the same lie arriving through a different door.
The useful question is not “is it up?” It is “what did I actually observe, and what am I inferring?”
Unattended changes the math
All of this matters more the moment nobody is watching.
A command you run is supervised by you. A daemon at 3am is supervised by nothing, and its failures are quiet. So when I built the always-on version, the first rule was not about capability at all:
First sight of a conversation is never actionable.
A watcher started against an inbox with years of history, that treats “I have not seen this before” as “this is new,” will answer all of it. Everyone who has ever messaged that number gets a reply, at machine speed, before anyone can intervene. So the first pass records a watermark and says nothing. A thread becomes live only from the next message onward.
That single rule is worth more than every clever thing in the file. Its companion is a hard cap — four replies per conversation per hour — which exists for exactly one purpose: when something upstream goes wrong, it stops at four instead of four hundred.
And a corrupt state file reads as empty, not as a clean slate, because “everything is new again” is the same catastrophe as a bad first run.
The default answer is no
There is a bias here worth naming. Every rule above fails toward silence.
A watcher that stays quiet when it is unsure costs you a delayed reply. A watcher that guesses costs you a hundred texts to a stranger at three in the morning. Those are not symmetric, and systems that act in the world should be built as if they are not.
Fail closed. Say what you observed. Keep the third state.
The bug is almost never that the machine did nothing. The bug is that it was sure.