Duplicate task-notification responses after TaskOutput consumption
Bug Description
When background bash tasks are consumed via TaskOutput(block: true), the task-notification messages still remain in the command queue and trigger redundant model response turns after the main turn completes.
Reproduction
Prompt:
Run 5 commands in the background simultaneously: each sleeps 3 seconds then outputs 1 through 5. Wait for all to complete and tell me the results.
Expected Behavior
The model uses TaskOutput(block: true) to wait for all 5 tasks, summarizes the results in one response, and the conversation ends cleanly.
Actual Behavior
After the model's summary response, 5 stale task-notification messages are dequeued one by one, triggering an additional redundant model turn that says something like "All 5 background commands confirmed complete" — repeating what was already said.
Root Cause
Race condition between enqueueShellNotification and TaskOutput's notified flag.
In LocalShellTask.tsx (lines 226-243), when a background task completes:
1. updateTaskState → status = 'completed' (sync)
2. enqueueShellNotification → checks notified flag, enqueues notification (sync)
Steps 1 and 2 run synchronously in the same microtask. Meanwhile, TaskOutput's waitForTaskCompletion (TaskOutputTool.tsx:122-137) polls every 100ms. By the time it detects the status change and marks notified: true (line 272-275), the notification has already been enqueued in step 2.
The notified flag only prevents duplicate enqueue — it does not remove notifications that are already in the queue.
Additionally, these notifications have priority: 'later' (LocalShellTask.tsx:169), so they are not consumed by the mid-turn drain in query.ts:1570-1571 (which only drains 'next' priority unless a Sleep tool was used). They sit in the queue until the turn ends, at which point useQueueProcessor dequeues them and triggers new model response turns.
Impact
- Redundant model turns with no useful content ("Already processed, no action needed")
- Wasted tokens (~50K+ cache read input tokens per redundant turn)
- Confusing UX — the model appears to talk to itself after completing the task
- Scales linearly with the number of background tasks (N tasks = N redundant notifications)
Suggested Fix
A few possible approaches:
- Remove consumed notifications from queue: When
TaskOutputsuccessfully retrieves a task result, also remove any matchingtask-notificationfrom the command queue by task ID. - Filter at dequeue time: In
processQueueIfReadyoruseQueueProcessor, check the task'snotifiedstate and whether the notification was already consumed viaTaskOutputbefore sending it to the model. - Promote priority: Change background bash task notifications from
'later'to'next'so they get consumed by the mid-turn drain in the same turn asTaskOutput, avoiding the post-turn dequeue path entirely.
Environment
- Claude Code version: 2.1.119
- Model: claude-sonnet-4-6
- OS: macOS Darwin 24.5.0
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗