My self-healing monitor took the business phone line down for two days. It was working perfectly the entire time.
Abid Ali · 10 September 2026 · github.com/buildwithabid
I run a small company-formation business. Clients reach me on WhatsApp, and two bridges — one for clients, one for alerts — run headless on a single VPS under systemd. A sweep every twenty minutes checks that both are healthy. If one fails the check, the sweep restarts it.
That last sentence is the bug. It took me two days and a dark client-facing number to see it.
The client bridge stopped answering. I opened WhatsApp on my phone, went to Linked Devices, and scanned the QR the bridge was showing. It linked, then died. I scanned again. It linked, then died.
I assumed a corrupted session profile — the bridge drives a headless Chromium, and a half-written profile is a known way for it to hang. So I wiped the profile and re-linked. It worked for a few minutes and died again.
The log had been telling me the answer all day:
09:37:12 bridge 4712 unhealthy → restart
09:58:40 bridge 4712 unhealthy → restart
10:19:55 bridge 4712 unhealthy → restart
10:41:08 bridge 4712 unhealthy → restart
…and on, every twenty-one minutes, until 12:46.
Twenty-one minutes. The sweep interval was twenty. The extra minute was the sweep's own runtime.
When a symptom and a repair loop share a period, suspect the loop.
Linking a WhatsApp bridge is not instant. You scan the code, and the session then syncs message history in the background before the bridge reports itself ready. That sync takes longer than you would guess.
During the sync, the bridge fails a deep health check — because it genuinely is not ready yet. My sweep saw an unhealthy bridge and did what I had told it to do. It restarted it, throwing away the scan I had just made, and leaving a half-written profile behind.
Then the next sweep found a bridge with a corrupt profile, which failed the health check, so it restarted that too.
A closed loop. And from the outside it looked exactly like the thing everyone says about this category of tool: WhatsApp automation is unreliable.
There was a second bug wearing the same costume: two sweeps could overlap, and each would independently decide to restart the same bridge.
The comment above the restart call — written by me, months earlier — said:
a restart is harmless, the session survives
True when a bridge is linked. False in exactly the window where the repair fires.
My healer could tell healthy from unhealthy. It could not tell broken from a human is part-way through fixing it. Those look identical to a health check, and they demand opposite responses.
A repair that cannot see the human will fight the human, and it will always win. It is faster, it never gets tired, and it runs on a timer.
The question I now ask before adding any auto-repair: what state does a human-in-the-loop occupy, and have I excluded it? Almost every self-healing system has one. Pairing, provisioning, a migration someone is running by hand, a certificate mid-renewal. If the healer cannot name that state, it will eventually stamp on it.
- Never restart during qrorcodestate. That is a human mid-fix. Alert instead.
- A grace window after authentication, while the session syncs.
- Never restart a bridge that has been up under 600 seconds. A crude rule that covers cases I have not thought of yet.
- Back off after three attempts in a day, and hand over to a diagnostic script with a human's name on it.
- A lock, so two sweeps cannot repair the same thing at once.
- Record the attempt before acting, not after — a sweep killed mid-repair still has to count, or the backoff never triggers.
With the loop fixed, the next failure was quieter and more interesting. The sweep's own state file said both bridges were down. Both bridges, asked directly, said ready.
The monitor had cached a judgement and then trusted its own cache over the thing it was monitoring. Health is now read from the service on every check, never inferred from stored state.
A monitor that trusts its own cache will eventually tell you a running system is dead — or, far worse, that a dead one is running.
I thought I had learned this. Then the alert bridge sat in a QR state for twenty-four hours and I did not know, because every check I had said it was fine.
$ systemctl --user status wa.service
Active: active (running) since Tue 08:30:44; 21h ago
The process was alive. The WhatsApp session inside it was dead. Meanwhile 433 lines of undelivered alerts and drafted client replies piled up in a log nobody was reading, and the escalation path had one channel — so when it broke, the thing that would have told me was the thing that was broken.
The heartbeat had been right the whole time. Every beat for twenty-four hours carried bridges.assistant: false. I was reading the wrong signal.
Monitor the session, not the process. active (running) answers a question you were not asking.
Two further fixes followed: the escalation channel now falls back to the other bridge, and a dead transport no longer consumes a message's retry budget — because a channel being down is not the same as a message being unanswerable, and my code had been treating them identically. Two failed sends and a drafted reply was marked handled. Not queued. Deleted.
Every one of them was a system confidently answering a question slightly different from the one being asked. Is it healthy? instead of is someone fixing it? Is the process up? instead of is the session alive? Did the send fail? instead of why did it fail?
None of these were hard problems. They were all precise ones, and precision is not what you reach for when you are writing the monitoring rather than the product.
The guarded-write design these systems now use is open source, MIT: Housewarden — a household-operations MCP server where every action that changes something is previewed, confirmed and audited.
Who wrote this. I'm Abid Ali. I build production MCP servers — the kind where every write is previewed before it happens, confirmed when it matters, and recorded in a chain you can verify. Not as a demo: I run one over live invoices, client prices and statutory filing deadlines every working day, which is how I came to learn everything above the hard way.
Housewarden is the public worked example, MIT licensed — 31 tools, one guard on every write, and a npm run e2e that drives it with a real MCP client and prints a pass/fail table so you don't have to take my word for any of it.
Available for MCP work — tool surface reviews, production builds, and keeping them running afterwards. What that looks like, with scope and fixed prices: The Write Path