Symptom: When a background agent completes while the main session is idle (between turns, waiting for stdin/FIFO), the <task-notification> sits in the queue indefinitely until the next user message triggers a turn. Observed gap: 1m51s idle with notification enqueued but undelivered.
Root cause (traced in v2.1.86-dev.20260326):
In runAsyncAgentLifecycle:
completeAgentTask(agentResult, rootSetAppState); // task.status → "completed" → hasRunningBg = FALSE
let handoffWarning = await classifyHandoffIfNeeded(...); // async (runs when mode === "auto")
let worktreeResult = await getWorktreeResult(); // async
enqueueAgentNotification({...}); // NOW hasQueued = TRUE
The do-while in runHeadlessStreaming polls every 100ms:
do {
await drainCommandQueue();
let hasRunningBg = getRunningTasks(state).some(...);
let hasQueued = hasCommandsInQueue();
if (hasRunningBg || hasQueued) { ...; await sleep(100); }
} while (waitingForAgents);
During the two await gaps in runAsyncAgentLifecycle, a poll tick fires, sees hasRunningBg=false && hasQueued=false, exits the do-while, emits result, and run() returns idle. The later enqueuePendingNotification (priority later) fires queueChanged.emit(), but stream-json's subscribeToCommandQueue handler only aborts for priority="now" — it doesn't re-trigger run().
REPL doesn't hit this: useSyncExternalStore(subscribeToCommandQueue) + useEffect → processQueueIfReady means any queueChanged emission triggers a drain.
Suggested fix: Reorder runAsyncAgentLifecycle to call enqueueAgentNotification before completeAgentTask sets status="completed" — the 100ms poll never sees both false. Alternatively: add a run() trigger to runHeadlessStreaming's subscribeToCommandQueue callback for any enqueue (not just priority=now), guarded by if (!running).
Repro trace: session 0e97d6af-ff32-4b68-8375-792a79ecbcdc, transcript lines 27852–27927. Enqueue at 20:43:08, dequeue at 20:45:52 (after next user prompt's turn).
Symptom: When a background agent completes while the main session is idle (between turns, waiting for stdin/FIFO), the
<task-notification>sits in the queue indefinitely until the next user message triggers a turn. Observed gap: 1m51s idle with notification enqueued but undelivered.Root cause (traced in v2.1.86-dev.20260326):
In
runAsyncAgentLifecycle:The do-while in
runHeadlessStreamingpolls every 100ms:During the two
awaitgaps inrunAsyncAgentLifecycle, a poll tick fires, seeshasRunningBg=false && hasQueued=false, exits the do-while, emitsresult, andrun()returns idle. The laterenqueuePendingNotification(prioritylater) firesqueueChanged.emit(), but stream-json'ssubscribeToCommandQueuehandler only aborts forpriority="now"— it doesn't re-triggerrun().REPL doesn't hit this:
useSyncExternalStore(subscribeToCommandQueue)+useEffect→processQueueIfReadymeans anyqueueChangedemission triggers a drain.Suggested fix: Reorder
runAsyncAgentLifecycleto callenqueueAgentNotificationbeforecompleteAgentTasksetsstatus="completed"— the 100ms poll never sees both false. Alternatively: add arun()trigger torunHeadlessStreaming'ssubscribeToCommandQueuecallback for any enqueue (not just priority=now), guarded byif (!running).Repro trace: session
0e97d6af-ff32-4b68-8375-792a79ecbcdc, transcript lines 27852–27927. Enqueue at 20:43:08, dequeue at 20:45:52 (after next user prompt's turn).