← All Posts POST #11 · · 7 min read

Why I Took the LLM Out of My AI Assistant's Morning Report (And Taught It to Sound Like Homer Simpson)

What happens when an autonomous cron agent quietly stops calling its tools for eight days, why deterministic scripts beat LLM health checks, and how a two-stage Piper + RVC daemon delivers voice notes in under ten seconds.

#hermes#llm#tts#rvc#piper#monitoring#homelab

In the August update to the assistant post, I wrote with some satisfaction that the 7am morning briefing had become a proper agentic run. Homer (the Hermes Agent instance that replaced Clawdia) woke up on a schedule, called the Juniper, Aruba, Synology, and Home Assistant MCP servers, summarised what it found, and sent the result to Telegram.

Two weeks after I wrote that sentence, Homer spent eight straight mornings waking up at 7am, calling zero tools, and cheerfully making up a clean health report from memory.

That was the week I stopped using a language model to run scheduled monitoring checks.

Two Ways an Agentic Cron Job Fails

When you hand a recurring infrastructure check to an LLM with tool access, you get flexibility for free: you describe the check in plain English, and the model works out which tools to call and how to format the answer. You also get two failure modes that a twenty-line Python script simply doesn’t have.

Failure 1: The Context-Window Cliff

The first failure hit in early July. An MCP tool querying the wireless controller’s client table or a busy log endpoint occasionally returns a much larger payload than usual. In a multi-step agent loop, every tool output gets appended to the conversation history for the next turn. Halfway through the morning run, the stacked tool output plus the model’s own reasoning tokens blew past the local vLLM context limit.

When a normal monitoring script hits an unexpected payload, it throws an error and alerts. When an agent loop overflows its context mid-run, the run aborts before it reaches the Telegram step. From the outside, a crashed morning check looks identical to a quiet morning unless you’re separately monitoring the monitor.

Failure 2: Tool-Call Rot

The second failure, in mid-August, was much worse because it didn’t crash at all.

For eight days, the scheduled cron runner inside the agent container got into a wedged state where the prompt fired on time, the local model generated a reply, and the tool-execution loop never fired a single MCP call. Faced with a prompt asking for the morning infrastructure summary and zero tool outputs in context, the model did what language models do best: it wrote a completely believable morning report based on previous mornings in its session history.

Every switch link was up. Every disk was healthy. Every backup had succeeded. None of those things had actually been checked.

Finding out that your monitoring bot has been writing fan fiction about your storage array for a week is a great reality check. It forces you to ask what the LLM was doing in that cron loop in the first place.

The --no-agent Pivot: Code for Facts, LLMs for Conversation

The honest answer was: nothing that Python couldn’t do better.

A scheduled health check doesn’t have an open-ended goal. Every morning at 7am, I want the exact same twenty facts across four areas:

  1. Connectivity & Edge - Is the primary WAN on the expected gateway? What did the hourly speed test record? How hot is the 5G backup modem running? Did CrowdSec block anything other than routine internet background noise?
  2. Core Infrastructure - Are all four virtual-chassis port-links up on the Juniper core? Are all three Aruba access points up, and how is the RF noise floor? Are the Synology storage pools, SMART counters, and UPS healthy?
  3. Host, VMs & Backups - Are all Linux VMs reachable? Is the local vLLM service responding? Did both nightly host-to-NAS backup tasks write a fresh completion heartbeat overnight?
  4. Security & Watchlist - Did the 6.40am MAC-keyed discovery sweep spot any never-before-seen hardware?

None of those questions need probabilistic reasoning. Every single one has a deterministic API call, a clear pass/fail threshold, and a one-line output format.

So I rewrote all four scheduled jobs on the assistant VM - the 7am morning report, the hourly WAN failover watchdog, the hourly 5G thermal sampler, and the fifteen-minute CrowdSec watcher - as standalone scripts running with --no-agent. They query the appliance APIs and SSH endpoints directly, build the report in plain Python, and hand the finished text to the Telegram gateway purely for delivery.

Three things got better straight away:

  • Zero hallucination surface - If a check can’t reach an endpoint, the script prints FAIL with the HTTP status code or timeout. It can’t invent a disk temperature.
  • Independent section timeouts - Each of the twenty checks runs in its own wrapper. A slow NAS log query (helped along by a pre-warm job at 6.50am) can’t starve the switch check or blow up a shared context window.
  • A canary for the gateway itself - A small health check verifies that the scheduler and delivery pipeline are actually advancing their state timestamps.

Two Rules for Alerting Hygiene

Moving to deterministic scripts also made it easy to enforce a strict alerting contract across the lab:

  1. One scheduled message per day, at 7am - All routine status (backups, new MAC addresses from the 6.40am discovery sweep, WAN speed history, disk health, and cert expiry horizons) rolls into that single morning digest.
  2. Outside 7am, only edge-triggered critical alerts - An unscheduled message only fires when a critical system flips from healthy to broken (or recovers): primary WAN failover, NAS unreachable, an internal host triggering a security block, or the assistant gateway failing its own canary.

If my phone buzzes at 2.15pm, something actually needs attention. That single rule is worth more than any dashboard.

Teaching the Assistant to Sound Like Homer Simpson

While the scheduled jobs lost their LLM, interactive chat on Telegram kept the local model (Swift-Qwen3.8-27B on the RTX 5090) and finally got the voice the bot’s name had been promising since June.

Back in August, Homer was still speaking in a stock Scottish female Piper voice (en_GB-alba), which was fast, local, and completely wrong for an assistant named Homer. Fixing that without giving up local-only execution meant building a two-stage voice pipeline inside the assistant VM:

Text reply -> Piper TTS daemon (base voice) -> RVC voice conversion daemon -> OGG/Opus voice note

Stage 1: Keeping Piper Warm (and Fixing the Hum)

Piper TTS is quick, though spawning a fresh process and loading the ONNX model on every message adds pointless lag. Running Piper as a persistent local HTTP daemon keeps the base voice (en_US-joe-medium, picked because its pitch and cadence map cleanly onto a male cartoon voice) warm in RAM.

That left one funny phonetic bug. If you ask a bot named Homer to start a reply with “Mmmm, donuts…”, standard phonemizers look at Mmmm, decide it has no vowels, and spell it out letter by letter: “em em em em”.

Nothing kills the illusion faster than a voice clone spelling out a hum. A small pre-processor now catches /\b[Mm]{2,}\b/ before synthesis, splits the text into segments, and splices a synthesised humming vowel sound with a gentle pitch bend straight into the WAV stream between the spoken words.

Stage 2: Persistent RVC Conversion

The base WAV from Piper goes into a persistent Retrieval-based Voice Conversion (rvc-homerd) daemon running a 600-epoch RVC v2 model. Keeping the RVC weights, FAISS index, and Hubert feature extractor loaded in a warm daemon instead of a per-message CLI script cut synthesis time from over thirty seconds on CPU to under ten seconds for a normal reply.

The result lands in Telegram as a voice note: local Whisper transcription on the way in, local Qwen 3.8 tool calling in the middle, and local Piper + RVC voice conversion on the way out.

The Lesson

Once you have a capable local LLM and a stack of MCP servers, it’s tempting to route every bit of automation through the model just because you can. Interactive questions - “Which switch port is the living room AP on, and how much PoE power is it drawing?” - are great with an agent, because you don’t know the question ahead of time.

Scheduled monitoring is the exact opposite. You already know the twenty questions you want answered every morning, and the only thing an LLM adds to a cron job checking twenty booleans is a creative new way to lie to you when the runner gets stuck. Let Python check the gear at 7am - save the language model for the chat over coffee at 7.05am.