Skip to content

Background task terminal completion not consistently exposed as a typed message (stale active-task state / hang) #1019

@maxim092001

Description

@maxim092001

Summary

The SDK can leave consumers with stale active background-task state because terminal background-task completion is not consistently exposed as a typed task-lifecycle message.

In live testing with claude-agent-sdk==0.2.87 and bundled CLI 2.1.150, background Bash tasks sometimes finish by emitting only:

SystemMessage subtype=task_updated data.patch.status=completed

with no typed:

TaskNotificationMessage status=completed

Applications that track active task IDs from TaskStartedMessage and clear them only on TaskNotificationMessage then believe a finished task is still active.

Why it matters

ClaudeSDKClient.receive_messages() is open-ended for the persistent client — it does not stop at ResultMessage (receive_response() is the bounded helper). For background tasks, consumers must keep draining past the first ResultMessage if a task is still running, since the CLI can later emit task-completion events and an automatic follow-up assistant turn. But they also need to know when the task is actually finished.

If task_started is exposed as a typed TaskStartedMessage but terminal completion sometimes arrives only as a generic SystemMessage(task_updated), the lifecycle contract is inconsistent, and consumers using the active set to bound receive_messages() can hang until an outer timeout.

Reproduction / evidence

A harness ran 10 real SDK cases with this runner logic:

if isinstance(message, TaskStartedMessage):
    active.add(message.task_id)
elif isinstance(message, TaskNotificationMessage):
    active.discard(message.task_id)
elif isinstance(message, ResultMessage) and not active:
    break

Results:

PASS 1 simple_no_background
PASS 2 background_immediate_then_auto_followup
FAIL 3 background_wait_with_taskoutput
PASS 4 background_fail_immediate
PASS 5 two_background_tasks_immediate
FAIL 6 background_read_file_after_completion
PASS 7 background_no_followup_requested
PASS 8 background_longer_immediate
PASS 9 foreground_control
FAIL 10 background_then_explicit_task_output

The failing cases received a final ResultMessage, but the active set was stale because completion arrived as task_updated, not TaskNotificationMessage:

TaskStartedMessage id=bs2r8eew4
SystemMessage subtype=task_updated id=bs2r8eew4 status=completed
AssistantMessage text=BG_WAIT_FINAL
ResultMessage active=['bs2r8eew4']
TIMEOUT active=['bs2r8eew4']
TaskStartedMessage id=b1m21w89v
AssistantMessage tool=TaskOutput input={'task_id': 'b1m21w89v', 'block': True, ...}
SystemMessage subtype=task_updated id=b1m21w89v status=completed
UserMessage tool_result=<retrieval_status>success</retrieval_status>...
AssistantMessage text=EXPLICIT_TASK_OUTPUT_FINAL
ResultMessage active=['b1m21w89v']
TIMEOUT active=['b1m21w89v']

Passing cases emitted both a task_updated and a task_notification, so consumers correctly cleared the active ID.

Root cause

The parser maps:

system/task_started       -> TaskStartedMessage
system/task_progress      -> TaskProgressMessage
system/task_notification  -> TaskNotificationMessage
other system messages     -> SystemMessage

So system/task_updated stays a generic SystemMessage, even when its patch carries a terminal status:

{
  "type": "system",
  "subtype": "task_updated",
  "task_id": "b1m21w89v",
  "patch": { "status": "completed", "end_time": 1780405729183 }
}

That terminal state is lifecycle data but is not represented by a typed lifecycle message.

Expected behavior

If the SDK emits a typed start event for a task, it should also emit a typed terminal event for the same task_id, regardless of whether completion happened via automatic notification, TaskOutput, or another background-task path.

Preferred approach: expose terminal system/task_updated as a typed message — a TaskUpdatedMessage mirroring the TypeScript SDK's SDKTaskUpdatedMessage — and document the contract so consumers can clear active task IDs on a terminal status from either TaskNotificationMessage or TaskUpdatedMessage.

Environment

  • claude-agent-sdk==0.2.87
  • bundled Claude CLI 2.1.150

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    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