Summary
In headless (claude -p) mode, when a background subagent completes after the parent turn has already ended with stop_reason=end_turn, the harness injects a <task-notification> user message. That opens a follow-on turn which is immediately interrupted ([Request interrupted by user]), leaving a non-terminal user message as the session's last record. The terminal-state check then fails and the CLI emits result: error_during_execution, is_error: true, stop_reason: null.
Net effect: a turn that completed successfully — full assistant text, stop_reason=end_turn — is reported to the caller as a hard error, and the completed answer is discarded by any harness that trusts is_error.
Compounding it, the only entry in errors[] is the CLI's own internal [ede_diagnostic] marker, which the CLI filters out of its user-facing output — so a caller reading errors[0] gets a diagnostic header instead of a cause, and there is no cause text anywhere.
Environment
- Claude Code: 2.1.215
- Invocation:
claude --print --output-format stream-json --verbose (headless, no stdin left open)
- Model: claude-sonnet-4-6
- Host: Linux container
- Frequency: 4 occurrences in ~30 runs of one scheduled task (~13%); intermittent, timing-dependent
Reproduction
- From a
claude -p invocation, have the model spawn several Agent subagents (backgrounded by default).
- Have it end its turn while at least one subagent is still in flight — a text-only reply, which the CLI records as
stop_reason=end_turn. (In our case the model explicitly said it was "waiting for" the two outstanding arms.)
- Let one of those subagents finish a few seconds later.
The completion notification arrives after the turn has closed; the session ends in error.
Actual behaviour
Session transcript, final four records (times relative to the last one):
-1s assistant stop_reason=end_turn "<complete final answer> … waiting for <two outstanding arms>."
-1s user <task-notification> <task-id>…</task-id> <tool-use-id>toolu_…</tool-use-id>
<output-file>~/.tmp/claude-…/tasks/….output</output-file>
0s user text: [Request interrupted by user]
Result event:
{ "type": "result", "subtype": "error_during_execution", "is_error": true,
"stop_reason": null,
"errors": ["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=null"] }
Note there is no user in headless mode to issue that interrupt.
The relevant guard in the shipped bundle — the last message is the injected/interrupted user message, whose content is not all tool_result, and stop_reason is null, so it returns false:
function wao(e, t = null) {
if (!e) return false;
if (e.type === "assistant") { let r = Kq(e.message.content);
return r?.type === "text" || r?.type === "thinking" || r?.type === "redacted_thinking"; }
if (e.type === "user") { let r = e.message.content;
if (Array.isArray(r) && r.length > 0 && r.every(n => "type" in n && n.type === "tool_result")) return true; }
return t === "end_turn";
}
// if (!wao(bn, tt)) { yield { type:"result", subtype:"error_during_execution", is_error:!0, … } }
And the errors array is built marker-first:
errors: (() => { let Lt = lvt(), sn = ls ? Lt.lastIndexOf(ls) + 1 : 0;
return [`[ede_diagnostic] result_type=${pi} last_content_type=${Er} stop_reason=${tt}`,
...Lt.slice(sn).map((Lr) => Lr.error)] })()
while the CLI's own consumer discards exactly those entries:
let t = e.errors.filter((r) => !r.startsWith("[ede_diagnostic]"));
if (t.length === 0) return null;
Expected behaviour
A turn that reached stop_reason=end_turn with a complete assistant message should not be reported as error_during_execution because a background notification arrived afterwards and its follow-on turn could not run. Either:
- don't open a new turn for a background notification once the parent turn has terminated in
-p mode (or drain notifications before finalizing), or
- finalize on the last valid terminal message rather than the trailing interrupted one.
Separately: when the run genuinely is an error, errors[] should carry a cause. Today errors[0] is always the [ede_diagnostic] marker the CLI itself filters, so SDK/harness callers surfacing errors[0] show users a meaningless string.
Contrast with successful runs
Five control transcripts from the same task: runs that spawned more subagents (21 and 24 Agent calls) completed fine — because the model waited for every arm's tool_result before ending its turn. Zero <task-notification> records, zero interrupts, all ending assistant / end_turn. The single failing transcript is the only one with a notification, and it lands after end_turn. It is not about background subagents per se — only about a notification arriving once the turn has closed.
Related
Summary
In headless (
claude -p) mode, when a background subagent completes after the parent turn has already ended withstop_reason=end_turn, the harness injects a<task-notification>user message. That opens a follow-on turn which is immediately interrupted ([Request interrupted by user]), leaving a non-terminal user message as the session's last record. The terminal-state check then fails and the CLI emitsresult: error_during_execution, is_error: true, stop_reason: null.Net effect: a turn that completed successfully — full assistant text,
stop_reason=end_turn— is reported to the caller as a hard error, and the completed answer is discarded by any harness that trustsis_error.Compounding it, the only entry in
errors[]is the CLI's own internal[ede_diagnostic]marker, which the CLI filters out of its user-facing output — so a caller readingerrors[0]gets a diagnostic header instead of a cause, and there is no cause text anywhere.Environment
claude --print --output-format stream-json --verbose(headless, no stdin left open)Reproduction
claude -pinvocation, have the model spawn severalAgentsubagents (backgrounded by default).stop_reason=end_turn. (In our case the model explicitly said it was "waiting for" the two outstanding arms.)The completion notification arrives after the turn has closed; the session ends in error.
Actual behaviour
Session transcript, final four records (times relative to the last one):
Result event:
{ "type": "result", "subtype": "error_during_execution", "is_error": true, "stop_reason": null, "errors": ["[ede_diagnostic] result_type=user last_content_type=n/a stop_reason=null"] }Note there is no user in headless mode to issue that interrupt.
The relevant guard in the shipped bundle — the last message is the injected/interrupted
usermessage, whose content is not alltool_result, andstop_reasonisnull, so it returns false:And the
errorsarray is built marker-first:while the CLI's own consumer discards exactly those entries:
Expected behaviour
A turn that reached
stop_reason=end_turnwith a complete assistant message should not be reported aserror_during_executionbecause a background notification arrived afterwards and its follow-on turn could not run. Either:-pmode (or drain notifications before finalizing), orSeparately: when the run genuinely is an error,
errors[]should carry a cause. Todayerrors[0]is always the[ede_diagnostic]marker the CLI itself filters, so SDK/harness callers surfacingerrors[0]show users a meaningless string.Contrast with successful runs
Five control transcripts from the same task: runs that spawned more subagents (21 and 24
Agentcalls) completed fine — because the model waited for every arm'stool_resultbefore ending its turn. Zero<task-notification>records, zero interrupts, all endingassistant / end_turn. The single failing transcript is the only one with a notification, and it lands afterend_turn. It is not about background subagents per se — only about a notification arriving once the turn has closed.Related
Monitortool's return message advises text-onlyend_turn— deterministically kills the watched task in headless (claude -p) mode #81468 / [BUG]Monitortool's return message advises text-onlyend_turn— deterministically kills the watched task in headless (claude -p) mode #81476 —Monitor's return message advises a text-onlyend_turn, which in-pkills the watched background shell. Same family (headless +end_turn+ outstanding background work); there the task is killed, here a completed session is reclassified as an error.claude -pexits 0 "success" while the agent loop is mid-task (opposite symptom: under-reporting).