Blog / Engineering

Keeping the Chat Alive When a Phone Sleeps

Phones sleep, switch tabs and drop networks mid-build. Two bugs made Mythex's chat stall or show half a reply afterwards. How we found and fixed both.

Mythex Team · 2026-09-29 · 4 min read

Mythex's agent can work for minutes on one request, and you may well be watching from a phone — which locks, switches apps and changes networks while it works. Two bugs made that go wrong. In one, the chat treated every "the page is visible again" signal as proof its connection had died, and looped until it sat on "Connecting…". In the other, a phone that slept through a new chat's first reply came back to only the first few pieces of it, until a manual refresh. Both are fixed.

How the chat follows a running turn

While the agent works, the chat holds open a stream from our servers: each step — a thought, a tool call, a piece of the reply — arrives as an event. If the stream drops, the chat reconnects and the server replays recent events from a buffer that holds the last 120 seconds. When the turn ends, the chat also asks the server for the saved history and reconciles what it showed with what was stored. That last step catches anything the stream missed.

To tell a live connection from a dead one while nothing is happening, the server sends a small "ping" every 15 seconds.

Bug 1: waking up killed a healthy stream

Browsers tell a page when it becomes visible again: after a tab switch, after the screen unlocks. We used that as a cue to check the connection — and in practice, to abort the stream and reconnect, assuming it had died while hidden.

On phones and in embedded browsers, these events fire far more often than streams die — every tab switch, every unlock, and in some embedded browsers every couple of seconds or so. Each one aborted a perfectly healthy stream. The chat reconnected, replayed, got aborted again, and ended up sitting on "Connecting…". Then it said the turn had ended while the agent was still working.

The fix: judge the stream by whether it's talking

The chat now records when each project's stream last received anything at all — events and the 15-second pings alike. A "visible again" signal only aborts a stream that has been silent for 25 seconds: longer than the ping interval plus a slow network hop, so a live stream never qualifies. Tests check that a ping counts as a sign of life, and that a stream that just pinged is not treated as dead.

The rule we took from it: a page's visibility says something about the page, not about the network connection. Ask the connection.

Bug 2: half a reply after the phone woke

This one we could reproduce every time in the iOS simulator:

  1. Start a new chat and send a request that takes a while.
  2. Let the phone sleep through the build.
  3. Come back after the turn has finished and the two-minute replay buffer has expired.

The chat showed the first few blocks of the reply, marked as finished — and stayed that way until you refreshed the page.

Why

A new chat doesn't have a thread — its stored conversation — until the first message creates one. That happens inside the send function, after send had already captured the reconcile step, which still thought there was no thread. So at the end of the first turn in a new chat, reconcile saw no thread and quietly returned without asking for anything.

Usually nobody noticed, because the stream had delivered the whole turn anyway. But a phone that slept through the turn and came back after the 120-second replay buffer had expired couldn't get the missing events from the stream — and the reconcile that should have filled the gap had never run.

The fix

  • Send tells reconcile which thread the turn belongs to, so a new chat's first turn is reconciled like any other. If you've opened a different chat by the time the answer comes back, the result is dropped rather than shown in the wrong place.
  • A failed reconcile is tried again. A phone waking on a bad connection can find the turn over and then fail to fetch it. That's now remembered, and the chat asks again when the tab is shown again or the network comes back.
  • "Worked for" is honest. Each reply shows how long the agent worked. If the tab never saw the server's end-of-turn message, it used to fill that in from its own clock — which ran from Send until the phone woke up, time away included. That figure is now marked as an estimate, and replaced by the stored duration once history arrives.

What we took from it

  • Test on the device people actually use, the way they use it. Both bugs lived in the gap between "a stream that works" and "a phone in a pocket".
  • A fallback that never runs isn't a fallback. The reconcile step existed to catch exactly this case, and silently skipped it.
  • Retry the recovery, too. The moment a phone wakes is often when its connection is at its worst.
  • Don't show a guess as a fact. A duration from the wrong clock is worse than none; label it or replace it.

The same work also tidied a few things you'll see while a turn runs: shell steps are labelled by the command that does the work rather than cd, screenshots in the chat keep the top-left of the page and fit the column, and a new project picks up its generated name without a refresh.

If you're building your own app for phones, our guides on building a mobile-friendly web app and native vs progressive web apps cover the basics. More about the chat itself is in the chat docs.

Keep reading

  • A Failed Republish Never Deletes a Working Backend — A failed republish could delete an app's live API, or leave its broken new version running. Three fixes to how Mythex undoes a publish that goes wrong.
  • Alerts That Fire Once, Not Once per Server — A once-a-day alert reached us five times in one afternoon. Why in-memory dedupe breaks with two servers and a deploy, and how shared state fixed it.
  • Moving to Bigger Workspaces Without Making Anyone Wait — A quarter of our workspaces were stuck at 1 GB after we moved to 2 GB. Our first fix made one person wait seven minutes. What we changed, and changed again.
  • Billing Databases by What They Actually Use — We used to estimate each app's database cost, and the estimate was wrong both ways. Now every database is billed from measured usage, hour by hour.
  • Catching Apps That Crash Right After Publishing — Three publishes passed our health check while their API crashed seconds into every boot. The cause: counting crashes in a list capped at five entries.
  • Keeping Image-Heavy Agent Turns Within Memory — An agent turn that kept looking at catalogue scans ran out of memory at 952 MB. Three separate causes, each holding images too long, and how we fixed them.

Start building free · Templates · Docs