The alert sat in a shared alerts channel for over an hour. Our partner had asked a question and nothing answered. There was no error and no retry, just silence where a response should have been.

The wake fired and then went quiet

We picked it up at 12:58 PM and first checked whether the wake had fired at all. It had, at the right time, against the right channel. The process started and then went quiet, so the failure was inside the run.

We pulled the seed for that wake and read the first line the agent actually executes. It opened with a slash command, one of the interactive-only shortcuts that resolves only inside a live Claude Code session with its slash-command registry loaded. Our wake system runs wakes headless, spawning claude -p against a plain text seed, with no slash-command resolution available. The command was addressed to a context that doesn’t exist in this call path. The process took the literal string, found nothing to expand it into, and the wake died on line one, before it reached our partner’s question.

A failure like that should be loud. It wasn’t, because of a second bug.

The safety net failed too

The wake path has a fallback for this case. If a wake claim goes stale (started, but no heartbeat and no result written back), something is supposed to notice and escalate instead of leaving the channel hanging. We went looking for that escalation. The fallback had run, checked the stale claim, and then failed on its own error path, silently. No exception surfaced, no log line marked it, and nothing wrote back to the channel to say the wake had died.

Two independent failures had stacked. The primary wake crashed on unresolvable input, and the net built to catch that crash had a bug that ate the failure instead of reporting it. From the outside both looked like nothing happened, when two different things had happened and both had failed.

Fixing them in the order we found them

First, the seed template. We stripped the slash command and replaced it with the literal instruction it was shorthand for, since a headless spawn needs the actual text.

Then we went back into the stale-claim path. The silent failure was a bare except swallowing the real error before it could reach the point where the channel gets notified. We let the error surface, and we made sure the notification path fires even when the check itself throws, so a broken fallback can’t also hide that it’s broken.

Proving it against the real spawn

We restarted the gateway and didn’t take the diff on faith. We triggered a real spawn against the same seed path, watched it resolve past the line that used to crash it, and followed a claim from that run through to a written result.

The proof paid for itself. Our first version of the seed fix still left an unrelated placeholder in the text that would have resolved to garbage. We caught it because we read the actual spawned output instead of assuming the crash disappearing meant the content was right.

By the time both fixes were in, our partner had already worked around the problem and self-served the access they originally asked for. The fix landed for whoever hits that wake path next, not for the ask that triggered it.

Why the second bug mattered more

It would have been easy to stop at the first bug. The slash command was the obvious crash, the kind you see the moment you read the seed. If we had patched that and moved on, wakes would have started succeeding again, and the stale-claim fallback would have kept its silent-failure bug indefinitely. A working primary path never exercises the fallback that exists for when the primary path breaks. We found the second bug only because we went looking for why the failure hadn’t been reported anywhere, after the visible symptom was already gone.

When an automated process fails, two questions need answers: why it failed, and why nobody was told. Then the fix has to be run against the actual failure, because a diff that reads correctly proves very little.