Resume CLI steering after an ask_user_choice answer

The ask_user_choice MCP tool blocks the CLI process (short-polling
ChoiceAnswers) until the user answers, after which the same
ChatStreamingAsync invocation continues. isUserChoice was set true when
the choice was raised but never reset, so both steering gates (the
tool-boundary kill and the turn-end check, each guarded by
!isUserChoice) stayed blocked for the rest of the run. A message thrown
in after answering was not drained at the next boundary; it lingered in
the queue and only arrived at chain end as a Close() leftover — the user
saw it "come in only at the very end" instead of in between.

Reset isUserChoice (and choiceToolCallId) as soon as the choice tool's
result comes back, before the steering check, so a message injected
during the choice pause takes effect at the next boundary just like
without a preceding choice. As a side effect the stop reason and error
handling after an answered choice are now correct (end_turn / real
errors surfaced) instead of staying awaiting_user_choice. CLI-specific:
the API path finalizes the chain on awaiting_user_choice, so it is
unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MelekiandClaude Opus 4.8 committed 2026-06-10 17:46:22 +02:00
1 parent de07b49b67
commit 1e37f078e0
1 file changed
+11
+11
View File
@@ -255,6 +255,17 @@ public partial class AnthropicService
isError = evt.ToolResultIsError
}, sseCt);
// ask_user_choice wurde gerade beantwortet (Result des Choice-Tools kam zurück)
// → Choice-Pause aufheben: die CLI nimmt die Arbeit in DERSELBEN Invocation
// wieder auf, also muss Steering ab hier wieder greifen. Ohne diesen Reset
// bliebe isUserChoice für den Rest des Laufs true und ein Einwurf käme erst
// ganz am Chain-Ende (als Close()-Leftover) an statt am nächsten Boundary.
if (isUserChoice && choiceToolCallId != null && evt.ToolId == choiceToolCallId)
{
isUserChoice = false;
choiceToolCallId = null;
}
// Steering: Tool-Boundary erreicht – steht ein Einwurf an, Child killen
// (löst OperationCanceledException aus → Restart unten).
if (!segSteerTriggered && !isUserChoice