Summary
The query() async iterator drains cleanly without ever yielding a terminal SDKResultMessage for the main-agent turn when (a) that turn invoked the Agent tool / subagent, AND (b) the entry path supplies a push-based AsyncIterable<SDKUserMessage> with no further user messages buffered. The final assistant message is yielded with stop_reason: "end_turn", then the iterator simply ends. No result, no error.
This is distinct from #333 — that one hangs after the last tool_result; ours ends after a clean end_turn. Same neighborhood (missing terminal event), different surface.
Environment
@anthropic-ai/claude-agent-sdk 0.2.76 (we have not yet retested on 0.3.157 — see "Will retest on latest?" below)
- Subscription auth, model
claude-sonnet-4-6
- Container per run; one
query() per process
MessageStream is a custom push-based AsyncIterable<SDKUserMessage> that stays open between IPC-arrived user messages, specifically to keep isSingleUserTurn=false and allow subagents to run
Trace evidence
A scheduled-task tick produced this session JSONL (parent-level messages only — subagent abstracted as a single tool_use/tool_result pair):
| # |
type |
detail |
| 4 |
assistant |
thinking only, stop_reason=null (partial) |
| 5 |
assistant |
tool_use: Agent (subagent invocation), stop_reason=tool_use |
| 6 |
user |
tool_result (subagent return) |
| 7 |
assistant |
thinking only, stop_reason=null (partial) |
| 8 |
assistant |
~5,320 output tokens of final text, stop_reason=end_turn |
No type: "result" entry follows line 8. The iterator in our consumer ends cleanly (no error thrown, no hang) with resultCount === 0.
The final assistant turn's content is well-formed and complete — the model finished its work. The SDK just never emitted the corresponding SDKResultMessage, so a consumer that relies on result as the signal to flush output gets nothing.
Trigger conditions
Reproduces consistently when all three hold:
- The main agent invokes the built-in
Agent tool / subagent at least once in the turn
- After the subagent returns, the main agent produces additional assistant content (text or further tools)
- The driving
AsyncIterable<SDKUserMessage> has no follow-up user message buffered when the main agent reaches end_turn
In our deployment this hit the scheduled-task entry path 100% of the time (8+ instances over a multi-hour window). When we rewrote the prompt to forbid the Agent tool and run the same logic inline (single turn, no subagent), the bug stopped reproducing. That is our current production mitigation alongside a defensive fallback emit in our consumer.
Expected vs actual
- Expected: after
stop_reason=end_turn, the SDK yields an SDKResultMessage (subtype success or error_*) and then closes the iterator.
- Actual: the iterator yields the
end_turn assistant message, then closes the iterator. No result message.
Possibly related
Will retest on latest?
Happy to retest on 0.3.157 if helpful — wanted to file with our actual evidence (0.2.76) first since the version delta is significant and didn't want to lose the repro context. The changelog between 0.2.76 and 0.3.157 doesn't mention a fix matching this symptom, but I may have missed it.
I can also share the full session JSONL and a minimal repro harness if useful.
Summary
The
query()async iterator drains cleanly without ever yielding a terminalSDKResultMessagefor the main-agent turn when (a) that turn invoked theAgenttool / subagent, AND (b) the entry path supplies a push-basedAsyncIterable<SDKUserMessage>with no further user messages buffered. The finalassistantmessage is yielded withstop_reason: "end_turn", then the iterator simply ends. Noresult, no error.This is distinct from #333 — that one hangs after the last
tool_result; ours ends after a cleanend_turn. Same neighborhood (missing terminal event), different surface.Environment
@anthropic-ai/claude-agent-sdk0.2.76 (we have not yet retested on 0.3.157 — see "Will retest on latest?" below)claude-sonnet-4-6query()per processMessageStreamis a custom push-basedAsyncIterable<SDKUserMessage>that stays open between IPC-arrived user messages, specifically to keepisSingleUserTurn=falseand allow subagents to runTrace evidence
A scheduled-task tick produced this session JSONL (parent-level messages only — subagent abstracted as a single tool_use/tool_result pair):
tool_use: Agent(subagent invocation), stop_reason=tool_useNo
type: "result"entry follows line 8. The iterator in our consumer ends cleanly (no error thrown, no hang) withresultCount === 0.The final assistant turn's content is well-formed and complete — the model finished its work. The SDK just never emitted the corresponding
SDKResultMessage, so a consumer that relies onresultas the signal to flush output gets nothing.Trigger conditions
Reproduces consistently when all three hold:
Agenttool / subagent at least once in the turnAsyncIterable<SDKUserMessage>has no follow-up user message buffered when the main agent reachesend_turnIn our deployment this hit the scheduled-task entry path 100% of the time (8+ instances over a multi-hour window). When we rewrote the prompt to forbid the
Agenttool and run the same logic inline (single turn, no subagent), the bug stopped reproducing. That is our current production mitigation alongside a defensive fallback emit in our consumer.Expected vs actual
stop_reason=end_turn, the SDK yields anSDKResultMessage(subtypesuccessorerror_*) and then closes the iterator.end_turnassistant message, then closes the iterator. No result message.Possibly related
Will retest on latest?
Happy to retest on 0.3.157 if helpful — wanted to file with our actual evidence (0.2.76) first since the version delta is significant and didn't want to lose the repro context. The changelog between 0.2.76 and 0.3.157 doesn't mention a fix matching this symptom, but I may have missed it.
I can also share the full session JSONL and a minimal repro harness if useful.