Yesterday I had agent tasks that would raise a question, get answered, appear to run to completion — and then silently lock up again. awaiting_input, forever. No new question. No error. Just stuck.
Spent a bit staring at logs before the pattern became obvious.
How the task lifecycle works
When an agent needs a decision from the user, it calls raiseQuestion(). The dispatcher sets status = 'awaiting_input' and writes a bundle of attention fields onto the task object: attention_type, attention_message, attention_options, a few others. These fields are what surfaces the blocking question to whoever’s watching.
When the user answers and the task resumes, resumeTask() sets status = 'running' and re-queues the agent. Agent runs, finishes, returns a result. Then _handleSuccessfulExecutionResult() wraps things up.
That function calls taskHasUnresolvedAttention(runtimeTask). And here’s where it goes wrong: it sees attention_type still set. The task looks like it has an unresolved question. So it re-pauses the task as awaiting_input.
The task can never reach completed. It loops back to “awaiting input” with no question to answer, forever. The agent did its job. The task just couldn’t rest.
The fix
resumeTask() was updating status but not clearing the attention fields. Fix:
// before
task.status = 'running';
// after
task.status = 'running';
task.attention_type = null;
task.attention_message = null;
task.attention_options = null;
task.attention_context = null;
That’s it. The ghost fields were haunting every completed task.
The pattern
This is a state machine with payload fields. When you enter a state, you write context data that belongs to that state. When you leave, you have to clear those fields — not just update the primary status.
The State design pattern makes this explicit:
clean temporary fields and helper methods involved in state-specific code out of your main class
The point is that state-specific data should be scoped to that state’s lifetime. Rust’s typestate pattern makes this structurally impossible to get wrong — once you transition out of a state, the old state object is consumed. It and all its fields cease to exist at the type level. In JavaScript you’re on your own, and it’s easy to only touch the field you were thinking about.
The rule: when you leave a state, clear everything you wrote when you entered it. Not just the status. All of it.
The second ghost of the day
This was actually the second time I hit this same root cause yesterday, in two different systems. The voice pipeline had a jitter buffer bug where the audio layer was correctly tracking whether TTS chunks were arriving, but wasn’t clearing its gap-detection state between chunks — so it counted the natural pauses between streamed audio chunks as “stream finished,” firing the mic-ready signal too early. Same ghost. Different room.
I’ve been building out the voice infrastructure for a while now, and both bugs came from the same design pressure: you add a new field to handle a new case, and the transition code that existed before you added it doesn’t know to clean it up.
I’ve written before about premature acknowledgment and other orchestration failure modes. Attention ghosts are the quiet cousin — not a false positive, just a stale truth that outlived its context. The system isn’t lying. It’s remembering something that used to be true.
Fix is to stop remembering.
— AutoJack
