Skip to content

Background task notification leaks into next turn after receive_response() #788

@garlanddiego

Description

@garlanddiego

Description

When a background task (spawned via run_in_background=true Agent tool) completes between turns, its TaskNotificationMessage leaks into the next receive_response() call. The model then responds to the stale task notification instead of the new user prompt.

Reproduction

import asyncio
import anyio
from claude_agent_sdk import (
    ClaudeSDKClient, ClaudeAgentOptions,
    AssistantMessage, TextBlock, ResultMessage,
    TaskStartedMessage, TaskNotificationMessage,
)

async def main():
    options = ClaudeAgentOptions(
        allowed_tools=["Read", "Glob", "Grep", "Bash"],
        permission_mode="bypassPermissions",
        cwd="/tmp",
    )

    async with ClaudeSDKClient(options=options) as client:
        # Turn 1: spawn a background task, end turn immediately
        await client.query(
            "Spawn a background agent (run_in_background=true) to run: sleep 60 && echo done\n"
            "Then immediately say 'Spawned.' and end your turn. Do NOT wait."
        )
        pending = set()
        async for msg in client.receive_response():
            if isinstance(msg, TaskStartedMessage):
                pending.add(msg.task_id)

        print(f"Turn 1 done. Pending tasks: {pending}")
        if not pending:
            print("No pending tasks — model waited for task. Re-run to reproduce.")
            return

        # Wait for the background task to complete
        print("Waiting 65s for background task to complete...")
        await asyncio.sleep(65)

        # Turn 2: simple question
        await client.query("What is 2+2? Just answer with the number.")
        async for msg in client.receive_response():
            if isinstance(msg, TaskNotificationMessage):
                print(f"*** BUG: Stale TaskNotification leaked! task_id={msg.task_id} ***")
            elif isinstance(msg, AssistantMessage):
                text = "".join(b.text for b in msg.content if isinstance(b, TextBlock) and b.text)
                if text:
                    print(f"Model response: {text[:100]}")
            elif isinstance(msg, ResultMessage):
                result = getattr(msg, "result", "") or ""
                print(f"Result: {result[:100]}")

anyio.run(main)

Note: The model may sometimes choose to wait for the background task before ending Turn 1 (no pending tasks). Re-run a few times to reproduce — it depends on model behavior.

Expected behavior

receive_response() for Turn 2 should only yield messages related to Turn 2's query. Background task notifications from Turn 1 should either:

  1. Be consumed by Turn 1's receive_response() (even after ResultMessage), or
  2. Be filtered out / delivered through a separate channel

Actual behavior

Turn 2's receive_response() yields a stale TaskNotificationMessage from Turn 1. The model sees this notification in its conversation context, responds to it ("The background task completed successfully...") instead of answering the new query ("2+2").

Observed message flow

Turn 2 receive_response():
  msg#1 TaskNotificationMessage task_id=btjnl316x status=completed  ← STALE from Turn 1!
  msg#2 SystemMessage subtype=init
  msg#3 AssistantMessage (tool use)
  msg#4 UserMessage (tool result)
  msg#5 AssistantMessage "The background task completed successfully..."  ← Wrong answer!
  msg#6 ResultMessage

Impact

In multi-turn applications (chat bots, agents), this causes:

  • Model answering questions about stale background tasks instead of the user's new message
  • "Answer wrong question" behavior that confuses users
  • Workarounds like client.stop_task() are unreliable ("No task found" errors)

Environment

  • claude-agent-sdk 0.1.50
  • Python 3.14.2
  • macOS

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions