You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A waking Agent.followup() can be accepted into next-turn but never run when it arrives after the turn loop's final inbox check and before the driver commits idle. The agent then reports idle while holding pending user work. Another unrelated waking message is required to resume it.
Tested on master at commit 47f943859bef60e4160492346772ded9b24f765a, including a real deepseek/deepseek-v4-pro-0813 AgentLoop run through OpenRouter and the real Bash tool.
Reproduction
Start an agent and let its first turn reach turn/end.
From a microtask scheduled by the turn/end observer, call:
queueMicrotask(()=>{agent.followup(createUserMessage({content: [{type: "text",text: "Use bash to write RETIREMENT_FOLLOWUP to retirement.txt.",}],source: {kind: "user"},}));});
Scheduling outside the synchronous session/event callback is necessary because nested session appends are intentionally rejected.
Await agent.whenIdle() and wait another 500 ms.
Inspect status, inbox, turn count, and the filesystem.
After sending a second, unrelated followup(), the parked request runs, retirement.txt contains RETIREMENT_FOLLOWUP\n, and the second message opens a third turn.
A waking message accepted before the current driver retires should either be claimed by that driver or latch a replacement driver. An idle agent should not retain waking next-turn work indefinitely.
Root cause
wakeDriver() returns for a live non-idle phase without setting wakeRequested; it only latches maintenance and post-abort wakes.
The loop's last pending-work check and the subsequent driver retirement leave a window in which work is inserted, its wake is ignored, and the driver transitions to idle without rechecking the inbox.
The repository already documents the same retirement-window hole for job completion notices and steering. This reproduction shows that ordinary user follow-ups share the same path and impact.
Impact
A user message can appear accepted but receive no response.
The agent reports idle while waking work remains pending.
Background completion notices and steering are affected by the same core state transition.
Recovery depends on an unrelated later wake.
Suggested fix
At the retirement boundary, atomically publish idle and then recheck pending waking work, or latch any waking send that arrives after the driver's final claim opportunity.
A regression test could schedule followup() in a microtask from turn/end, assert that the agent never remains idle with pending waking work, and verify that no second external wake is required.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
A waking
Agent.followup()can be accepted intonext-turnbut never run when it arrives after the turn loop's final inbox check and before the driver commitsidle. The agent then reportsidlewhile holding pending user work. Another unrelated waking message is required to resume it.Tested on
masterat commit47f943859bef60e4160492346772ded9b24f765a, including a realdeepseek/deepseek-v4-pro-0813AgentLoop run through OpenRouter and the real Bash tool.Reproduction
turn/end.turn/endobserver, call:Scheduling outside the synchronous
session/eventcallback is necessary because nested session appends are intentionally rejected.agent.whenIdle()and wait another 500 ms.Observed:
After sending a second, unrelated
followup(), the parked request runs,retirement.txtcontainsRETIREMENT_FOLLOWUP\n, and the second message opens a third turn.The deterministic run reported:
{ "strandedWhileIdle": true, "pendingNextTurnWhileIdle": 1, "recoveredOnlyAfterSecondWake": true, "finalTurnCount": 3 }Expected behavior
A waking message accepted before the current driver retires should either be claimed by that driver or latch a replacement driver. An idle agent should not retain waking
next-turnwork indefinitely.Root cause
wakeDriver()returns for a live non-idle phase without settingwakeRequested; it only latches maintenance and post-abort wakes.The loop's last pending-work check and the subsequent driver retirement leave a window in which work is inserted, its wake is ignored, and the driver transitions to idle without rechecking the inbox.
The repository already documents the same retirement-window hole for job completion notices and steering. This reproduction shows that ordinary user follow-ups share the same path and impact.
Impact
Suggested fix
At the retirement boundary, atomically publish idle and then recheck pending waking work, or latch any waking send that arrives after the driver's final claim opportunity.
A regression test could schedule
followup()in a microtask fromturn/end, assert that the agent never remains idle with pending waking work, and verify that no second external wake is required.All reactions