The way I actually work with Tim, my AI agent, is simple: I give it one big task, then I walk away. Make coffee, answer a customer, play with my kid. When the AI finishes, a little "ding" is supposed to pull me back to the screen.

For the last few weeks, that ding kept disappearing. I'd find myself flipping back to the tab every few minutes just to check whether the work was done — which kills the entire point of having an AI that runs on its own in the background.

I complained to Tim about it more than once: "The notification sound is gone again." Finally I said, "Seriously, go find out why it vanishes, and make it stop for good."

What Tim dug up was a root cause deeper than I expected. It wasn't a bug in our code at all — it was a rule built into the browser itself, one we'd been quietly fighting without realizing it.

The symptom — it rings once, then goes silent

First, where this sound lives: it's inside Tim Chat, the chat screen I use to talk to my agent every day (and the exact same one every Newton customer gets on their own server).

The logic is dead simple. If I switch away to another tab — so the chat is in the background — and the AI finishes its work, play a short alert. If the chat tab is sitting right in front of me, don't play anything, so it never gets annoying.

But here's the pattern I kept hitting:

  • Open a fresh chat, send a message, walk away, AI finishes → sound plays
  • But after I reload the page (hit refresh, or open the chat fresh in the morning) → send a task → walk away → AI finishes → dead silence
  • And sometimes, mid-conversation, the sound would just quietly stop working

The maddening part was that it didn't fail every time. It vanished at random — some days the ding showed up, some days it didn't — which made it nearly impossible to pin down.

The first clue — the sound dies on a server restart

Tim started by asking a sharper question than "where's the bug in the code?" — it asked, "exactly when does the sound disappear?"

Once it watched the pattern, two precise triggers fell out:

  • Every time I hard-reloaded the page
  • Every time I deployed new code and the service restarted — because on restart, the page automatically reconnects its WebSocket

That was the moment it became clear the problem wasn't the "should I play the sound?" logic. That logic was working fine; it issued the play command every time. The thing dying silently was the sound player itself.

Root cause — browser autoplay policy

The original alert was built with the Web Audio API. We didn't have an actual sound file — instead, JavaScript synthesized an 880Hz tone live, the moment it was needed (using an oscillator).

It sounds clever. No file to load, generated on the fly. But it carried a trap I never knew about.

Modern browsers (Chrome, Safari) enforce something called an autoplay policy — a page isn't allowed to make noise until the user has "touched" it first (a click, a keypress, anything), as a way of confirming "a real person is here, this isn't a site sneaking an ad at you."

A Web Audio context lives in one of two states: suspended or running. It only wakes up to running after a genuine user gesture. In the old code, we woke it on my first "send message."

The problem: every hard-reload and every WebSocket reconnect drops the AudioContext back to suspended, and it only wakes again on my next send. So the failure looked like this:

  • I open the chat in the morning (a reload) → context is suspended
  • I type a long task and hit send once → context wakes up… but wait
  • While the AI is working, I deploy → service restarts → WebSocket reconnects → context drops back to suspended
  • AI finishes, issues the play command → but the context is asleep → silence

It was a sound that depended entirely on timing lining up just so — which is exactly why it felt random and was so hard to catch.

(This is a different problem from when I had Tim fix the voice-typing feature that echoed every word back twice — that one was audio coming in, this is the alert going out — but both are small UX polish jobs inside Tim Chat that quietly grate on you every single day if you don't fix them.)

The fix — stop synthesizing, play a real file

Tim's proposal was to stop fighting the Web Audio context that keeps falling asleep, and flip the whole approach:

1. Generate one real sound file.

Tim created notify.wav — an 880Hz tone, the exact same pitch and volume as before, with the same exponential-decay envelope (it fades out smoothly instead of cutting off). It sounds identical to the old one. The only difference is it's now a real file instead of something synthesized on the spot.

2. Play it through a plain <audio> element.

An HTML <audio> element behaves differently from a Web Audio context: once you "unlock" it once with a user gesture, it stays unlocked. It doesn't fall back to suspended on every reconnect the way an AudioContext does.

So Tim unlocks it on my first send (it plays once, silently, to earn the browser's permission), and from then on it's ready to fire indefinitely — straight through WebSocket reconnects and service restarts.

3. Keep the original rule — only ring when the tab is in the background.

The document.hidden check stays exactly as it was. If I'm staring at the chat, it doesn't play (no annoyance). It only rings when I've stepped away — which is all I ever wanted.

The result — I tested it myself, and it passed

Here's a part I appreciated: Tim was upfront that it couldn't test this one itself. "I can't actually hear the sound in my environment — there are no speakers — so you'll have to be the one to play it and confirm."

I like that it told me plainly which parts it could verify and which it couldn't, instead of claiming "all done" about something it physically can't check. That's a habit I've been trying to train into it for a long time.

So I ran the test myself: reload the page → send a task → switch over to YouTube → wait for the AI to finish → "ding." Right on cue. I ran it again and again, and I even deployed and restarted mid-task — the sound came through every single time.

It passed. Gone for a week, fixed in one evening.

There's one small caveat Tim flagged honestly: after a full hard-reload, I still have to send one message to re-unlock the sound (that's the browser's autoplay rule, genuinely unavoidable). But in real use I send a message every time I open the chat anyway, so it never gets in the way.

The lessons I wrote down

1. "Play a real file" is sometimes tougher than "synthesize live."

The cleverer-looking option (generating audio in code, no file needed) isn't automatically the more durable one. In this case a dumb little .wav survives reconnects better than the much smarter Web Audio context ever did. Simplicity wins again.

2. A bug that "disappears sometimes" usually points at state, not logic.

When something fails at random, don't immediately start auditing your conditions for a logic error. Ask first: "is there some piece of state that's silently resetting itself without my knowing?" Here it was the AudioContext quietly dropping back to suspended on every reconnect.

3. Browser autoplay policy is a friend to understand, not an enemy to beat.

It exists to stop sites from blasting ads at you — good intentions. But you have to design around it: unlock once when the user touches the page, then hold that unlock tight. Don't let it fall asleep and then wonder why everything's quiet.

Why a tiny thing like this is worth a whole post

Because it's the difference between a tool that "works" and a tool you actually trust.

One notification sound sounds trivial. But it's the thing that lets me fire off a task and walk away without babysitting the screen — which is the entire heart of having an AI agent that runs on its own, rather than a chatbot you have to sit and watch.

That's why I was happy to let Tim burn an entire evening on a single "ding." These small details are exactly what stack up into a tool I rely on every day, instead of a toy I try once and abandon.

Want a private AI agent like this?

Everything I just described in Tim Chat — the chat screen, the notification sound, the voice-typing button, all of it — is exactly what Newton customers get too.

Newton is your own private AI agent on your own server, ready to go in 10 minutes. Your AI lives on a server that's yours alone — it can see your code and fix the same fiddly bugs I just walked through. And every time I polish something like this, every Newton customer gets the upgrade too.

If you want an AI agent that lives on your own machine, runs around the clock, and has someone improving it every single day, take a look at Newton. It sets up in 10 minutes, and you don't need to know a thing about servers. See how it works →

— Pond