Skip to content

Background subagents orphaned: runClaudeWithSdk breaks the query() loop on the first result (regression from #1339) #1499

Description

@conancain

Describe the bug

base-action/src/run-claude-sdk.ts (runClaudeWithSdk) consumes the query() async iterator and breaks on the first SDKResultMessage. That break was added in #1339 to stop the iterator hanging open in pull_request runs:

if (message.type === "result") {
  resultMessage = message as SDKResultMessage;
  // ...break to avoid the pull_request hang (#1339)...
  break;
}

Since the Agent SDK runs subagents as background tasks by default (Claude Code ≥ 2.1.198), breaking on the first result ends the run prematurely whenever the agent uses background subagents. The parent dispatches subagent(s) via the Agent tool, its dispatch turn ends, and the SDK emits a result while the subagents are still running — so runClaudeWithSdk breaks there, before the subagents finish and before the parent gets the follow-up turn(s) to use their results. The subagents are orphaned and the parent never produces its final output. The run reports success (a result was recorded), so it fails silently.

To Reproduce

Steps to reproduce the behavior:

  1. Run the base-action SDK path (runClaudeWithSdk) in a pull_request context.
  2. Give the model a prompt that dispatches ≥1 subagent via the Agent tool as a background task (the default) and then acts on the result — e.g. "dispatch reviewer subagents in parallel, then post a consolidated summary".
  3. Watch the SDK message stream; the run terminates right after the parent's first result:
Agent(subagent A)   -> background_tasks_changed:[A]     task_started A
Agent(subagent B)   -> background_tasks_changed:[A,B]   task_started B
result  ("dispatched; waiting for reviewers")   <-- runClaudeWithSdk breaks HERE
[A completes] -> task_notification A             (never delivered to the loop)
[B completes] -> task_notification B             (never delivered)
[parent consolidation follow-up turn]            (never runs)
  1. The parent's final output (in our case a consolidated review comment) is never produced; only the pre-dispatch placeholder remains.

Expected behavior

The run should wait for the background subagents to finish and for the parent's follow-up turn(s) to produce the final output before terminating — i.e. break on the final result, not the first one, while still avoiding the #1339 hang. A naive "break only once no background tasks are live" (tracking background_tasks_changed) still races: when two subagents finish near-simultaneously, the live set drains to empty while a per-task task-notification follow-up turn (the one that consolidates) is still queued, and the loop breaks on an interim follow-up result before the consolidation turn runs. The robust fix likely needs the SDK's own knowledge of pending follow-ups (e.g. don't treat a result as terminal while background tasks are pending or their follow-ups are undelivered; or expose whether an origin: { kind: "task-notification" } result is the last one; or make runClaudeWithSdk background-task-aware upstream so every integrator benefits).

Screenshots

N/A (headless run) — the SDK message-stream trace above is the evidence.

Workflow yml file

N/A — this isn't run through GitHub Actions. We invoke the action's base-action SDK path (src/entrypoints/run.ts) headless in our own CI, in a synthesized pull_request context, with the agent configured to dispatch reviewer subagents in parallel. The bug is in run-claude-sdk.ts's loop and is independent of the workflow layer / provider.

API Provider

[ ] Anthropic First-Party API (default)
[x] AWS Bedrock
[ ] GCP Vertex

Additional context

  • Versions: claude-code-action v1.0.170 (repro'd); the break is unchanged on main as of filing. @anthropic-ai/claude-agent-sdk 0.3.x. Claude Code CLI 2.1.206 (background subagents default since 2.1.198).
  • Root cause is the interaction between fix: break SDK iterator after result message to prevent hang in pull_request runs #1339's break-on-first-result and background subagents; it's provider-independent.
  • Workaround we currently carry: don't break while background_tasks_changed shows live tasks, and after they drain wait for the consolidation output before breaking, with a bounded fallback to preserve the fix: break SDK iterator after result message to prevent hang in pull_request runs #1339 anti-hang guard. This took our failure rate from ~12% of multi-subagent runs to 0 in production, but it's a heuristic (the consumer can't reliably tell an intermediate follow-up result from the final one) — hence this report. Happy to send a PR as a starting point.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingp2Non-showstopper bug or popular feature request

    Type

    No type

    Fields

    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