🤖 fix: keep ask_user_question waiting across restart #1152
Merged
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fixes a restart edge case where
ask_user_questionwas treated as an interrupted stream.When Mux is closed while the agent is blocked on
ask_user_question, we now treat that tool call as a durable waiting-for-input state:On restart, answering the questions now works even though the in-memory pending tool call is gone:
partial.json(or chat history) and emits a synthetictool-call-end📋 Implementation Plan
🤖 Plan: Persist
ask_user_questionas a true “waiting for input” stateGoal
When a workspace is blocked on
ask_user_questionand the user closes/reopens Mux, do not treat that as an interrupted stream that must be auto-resumed. Instead:Esccancel/interrupt while we’re awaiting anask_user_questionRecommended approach (minimal + consistent)
Make
ask_user_question“resume-safe” by treating it as a special waiting state (not an interruption).What changes, behavior-wise
After app restart with a partial message whose last part is an unfinished
ask_user_questiontool call:resumeStream().While actively awaiting
ask_user_question(stream is still “running” but blocked on user input):Esc/Ctrl+Cinterrupt keybind becomes a no-op for this state.When the user submits answers after a restart (no active stream exists anymore):
output-availablewith{ questions, answers }.tool-call-endevent so the renderer updates immediately.Why this works with the current architecture
partial.jsonwith the assistant message containing the tool call.interrupted, which triggers:we avoid re-running the LLM just to re-create questions.
Implementation steps
1) Frontend: classify
ask_user_questionin partial messages as “executing”Files:
src/browser/utils/messages/StreamingMessageAggregator.tsChange: In
getDisplayedMessages()tool status mapping:input-available && message.metadata.partial → status = "interrupted"toolName === "ask_user_question", treatinput-availableas"executing"even whenpartial.Also tighten
hasAwaitingUserQuestion():2) Frontend: suppress “Interrupted” + Retry + auto-resume for that state
Files:
src/browser/utils/messages/retryEligibility.tssrc/browser/utils/messages/messageUtils.tssrc/browser/components/AIView.tsx(optional defense-in-depth)Change:
hasInterruptedStream(...): if the last message is a tool message withtoolName === "ask_user_question"andstatus === "executing", return false.RetryBarrieruseResumeManagerauto-resumeshouldShowInterruptedBarrier(msg): return false for the same tool message type.AIView, also gateshowRetryBarrierby!awaitingUserQuestionfor extra safety.3) Frontend: disable interrupt keybind while awaiting questions
File:
src/browser/hooks/useAIViewKeybinds.tsChange:
aggregator?.hasAwaitingUserQuestion()is true, do not callworkspace.interruptStreamand do not toggleautoRetry.4) Frontend: stop advertising
Escfor this stateFiles:
src/browser/components/ChatInput/index.tsxsrc/browser/components/AIView.tsxChange (small UX polish):
awaitingUserQuestionas a prop toChatInputso the placeholder/hints avoid “Esc to interrupt” and instead reflect “Answer above / type a message to respond”.5) Backend: allow answering after restart (no active stream)
Files:
src/node/services/workspaceService.tsChange: make
answerAskUserQuestion(...)async and implement fallback:askUserQuestionManager, resolve it (existing behavior).partial.jsonviapartialService.readPartial(workspaceId).ask_user_questiontoolCallId, update that tool part tooutput-availablewith{ questions, answers }and write back viapartialService.writePartial.chat.jsonlviahistoryService.getHistoryand update viahistoryService.updateHistory.tool-call-endchat event usingsession.emitChatEvent(...)so the UI updates immediately.Important guardrails (defensive programming):
inputmatchesAskUserQuestionToolArgsshape before using it.6) Frontend: after successful submit, request resume immediately
File:
src/browser/components/tools/AskUserQuestionToolCall.tsxChange: after
answerAskUserQuestionsucceeds:CUSTOM_EVENTS.RESUME_CHECK_REQUESTEDwith{ workspaceId, isManual: true }.autoRetry=falsestate and resumes promptly.Tests
Frontend unit tests
src/browser/utils/messages/StreamingMessageAggregator.status.test.tsask_user_question→ tool status isexecutingandhasAwaitingUserQuestion()is true.src/browser/utils/messages/retryEligibility.test.tsask_user_questionexecuting →hasInterruptedStreamis false.Backend unit tests
toolCallId,{ questions, answers },Rollout / validation
ask_user_question.Escdoes not interrupt while awaiting questions.Net LoC estimate (product code only)
Alternatives considered
A) Persist/resume the actual in-flight model request
B) Make
convertToModelMessages(ignoreIncompleteToolCalls=false)forask_user_questionGenerated with
mux• Model:openai:gpt-5.2• Thinking:xhigh