stream-json: background-task notification doesn't wake idle session (race in runAsyncAgentLifecycle)

Resolved 💬 3 comments Opened Mar 27, 2026 by dholtmannrice Closed May 23, 2026

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) + useEffectprocessQueueIfReady 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).

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗