Duplicate task-notification responses after TaskOutput consumption

Resolved 💬 3 comments Opened Apr 24, 2026 by zhanjinfeng1222 Closed Apr 24, 2026

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:

  1. Remove consumed notifications from queue: When TaskOutput successfully retrieves a task result, also remove any matching task-notification from the command queue by task ID.
  2. Filter at dequeue time: In processQueueIfReady or useQueueProcessor, check the task's notified state and whether the notification was already consumed via TaskOutput before sending it to the model.
  3. Promote priority: Change background bash task notifications from 'later' to 'next' so they get consumed by the mid-turn drain in the same turn as TaskOutput, 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

View original on GitHub ↗

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