From late September to mid-October, my phone kept buzzing with the same alert while I was gaming: BRAIN DOWN. Jarvis, the assistant I run on my laptop, has a separate watchdog called Guardian, and Guardian was telling me the Brain had stopped sending heartbeats for two minutes or more. Between September 20 and October 13 it fired 50 times. They came in bunches, fifteen of them on October 4 alone, and every bunch landed on an evening I was playing games.
The obvious read was that the Brain, the part that talks to the language model, was crashing or hanging on a slow call. That read was wrong, and it took looking at the data instead of the alert to see it.
The alert named the wrong suspect
Jarvis records what it does as events in a SQLite database: heartbeats, scheduled jobs, paper trades, messages. So I pulled the events around one of the alerts, just before 1:00 a.m. on October 13. Heartbeats were arriving every eleven seconds as normal. Then gaps of 29 and 52 seconds started showing up, and then nothing at all for 116 seconds. It was not just the Brain's heartbeat. Every type of event went silent at the same moment, including Guardian's own heartbeat task.
Meanwhile Guardian's monitor, which runs on its own thread, kept working and sent the alert. That combination told me what was really happening. Jarvis runs on Python's asyncio, which means one event loop on one thread schedules nearly everything. If any code on that thread does slow work without letting go, everything else waits behind it. The Brain was not down. The whole kernel was frozen, and the Brain just happened to be the thing Guardian was watching.
Four things were holding the loop
Once I knew to look for synchronous work on the event loop, it was not hard to find. It was hard to accept how much of it there was.
- Every event was written to SQLite on the loop. Each one opened the database, set three pragmas, inserted a row, committed and closed. The log had 2,645 warnings in ten days of a single heartbeat taking between 0.4 and 2 seconds to save.
- The database had grown to 1 GB. It held 4.7 million rows, and 3.7 million of them were heartbeats. I had written a cleanup routine that purges heartbeats older than seven days. I had never wired it to run.
- One query ran every ten seconds without the index it needed. A health check asked for the newest Guardian heartbeat, and the index only covered the event type, so SQLite walked about 126,000 rows each time. With the file cached, that took 0.12 seconds. Cold, it took 6.5.
- Text to speech blocked the queue. When Jarvis spoke a reply, it waited for the voice to be synthesized and played before handling the next event.
None of these is dramatic on its own. On a quiet machine they were mostly invisible, which is exactly why they survived as long as they did.
Why games made it worse
A game takes memory, so Windows drops SQLite's file cache to make room. That is the difference between the 0.12 second query and the 6.5 second one. The game also streams assets from disk, which slows every write and commit, and it competes for the CPU. Every one of those synchronous calls got slower at once, and all of them ran on the single thread everything else depended on. Short tests on an idle machine never showed it. A real evening of gaming did.
What I changed
The fixes were not clever. They mostly moved work off the one thread that cannot afford it.
- A dedicated writer thread now owns one long-lived database connection, so the event loop never touches SQLite to record an event. The order of events is unchanged.
- Heartbeats still go to everything that listens for them, but they are no longer saved. Nobody needs 3.7 million of them.
- A combined index on event type and time turns the every-ten-seconds question into a quick lookup.
- The cleanup job that was written but never wired now runs every night at 3:40 a.m., off the loop.
- Spoken replies go through their own queue, so speech no longer holds up everything behind it.
- A small watchdog thread pings the event loop every second. If the loop does not answer within five seconds, it writes down the exact file and line the loop is stuck on.
That last change mattered most for what came next. Within a few days the watchdog caught a stall I would never have guessed: creating a new secure connection setup for each web client was holding the loop for five seconds or more on this machine. Jarvis now builds that once at startup and reuses it. A problem that would have taken weeks of guessing took one log line.
So far there has been one more Brain alert, on October 17, and none since. I still catch myself wanting to call it solved. The honest version is that the watchdog has flagged only a couple of single five-second blips since then, and each one came with the line that caused it.
What I would tell anyone building one
If your assistant runs on asyncio, assume anything synchronous on the loop will eventually hurt you, and assume it will happen under a load you are not testing. Two habits are cheap to start with. Give anything that reads or writes the disk its own thread from the beginning. And build the watchdog before you need it, because a frozen program cannot log why it is frozen. The thing that would report the problem is the thing that is stuck.
The other lesson is the one I keep relearning: an alert tells you what was being watched, not what broke. If I had chased the Brain, I would still be chasing it.
If you are building your own assistant, Hey Jarvis: Your Machine. Your AI. Your Rules. walks through the build this all runs on, and notes like these are what happened after it went into daily use. The method behind this whole hunt, stating the problem clearly before guessing at the cause, starts with the free worksheet at https://solved360.com/pages/free.