From 1e37f078e04799c07857a0410b423bb6c5b5d6d8 Mon Sep 17 00:00:00 2001 From: Meleki Date: Wed, 10 Jun 2026 17:46:22 +0200 Subject: [PATCH] Resume CLI steering after an ask_user_choice answer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Services/AnthropicService.Cli.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index d52e58b..578dcd1 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -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