fix(bridge): surface between-turn AskUserQuestion as its own card#274
Merged
Conversation
When AskUserQuestion fires between user turns (teammate / /goal /
continuation-burst follow-up), the PreToolUse hook in
PersistentClaudeExecutor has no `activeTurn` to piggy-back on. The
question text only landed inside the coalesced "Agent activity" body, and
the user's typed reply was treated as a fresh user turn — which then
blocked for 6 minutes on the still-hanging hook ("Thinking…" forever).
Fix: when the hook trips with no activeTurn, additionally emit a
`between-turn-question` event with the parsed questions payload. The
bridge subscribes alongside the existing `spontaneous` /
`continuation-turn` channels and:
- sends a dedicated question card via sendQuestionCard
- intercepts the user's next typed reply for that chatId in
handleMessage (before runningTasks / queue branches)
- routes the parsed answer back via executor.resolveQuestion(),
unblocking the hook and letting the SDK proceed
- tears the card down to error state if the executor is evicted
Multi-sub-question between-turn calls only surface the first question
(rare; logged) — same single-question-at-a-time pattern as runOneTurn.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7 tasks
floodsung
added a commit
that referenced
this pull request
May 14, 2026
`handleContinuationTurn` renders the agent's response to an SDK-injected `<task-notification>` (background bash return etc.) as a fresh streaming card. Before this fix, if the agent invoked AskUserQuestion *inside* that continuation turn, the stream processor would set `state.pendingQuestion` but the loop body just kept updating the main card with the question text inline — no dedicated question card, no reply routing. The user's typed reply was treated as a fresh user turn that immediately blocked for 6 minutes on the still-hanging hook. Fix: reuse the standalone between-turn question pipeline from #274. When the continuation stream yields `waiting_for_input` + `pendingQuestion`: - update the main card with a "_Waiting for your answer to the question card below…_" hint (drops `pendingQuestion`, mirrors the pattern in runOneTurn) - call `handleBetweenTurnQuestion` to surface a dedicated v1 question card and register the toolUseId for reply interception in handleMessage - track surfaced toolUseIds in a per-stream Set so we don't re-send the card on every subsequent delta while the same question is still waiting Same `executor.resolveQuestion()` path unblocks the hook; the continuation stream then continues normally. Co-authored-by: Flood Sung <floodsung@xvirobotics.ai> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
SimonYeyi
pushed a commit
to SimonYeyi/metabot
that referenced
this pull request
May 26, 2026
…irobotics#274) When AskUserQuestion fires between user turns (teammate / /goal / continuation-burst follow-up), the PreToolUse hook in PersistentClaudeExecutor has no `activeTurn` to piggy-back on. The question text only landed inside the coalesced "Agent activity" body, and the user's typed reply was treated as a fresh user turn — which then blocked for 6 minutes on the still-hanging hook ("Thinking…" forever). Fix: when the hook trips with no activeTurn, additionally emit a `between-turn-question` event with the parsed questions payload. The bridge subscribes alongside the existing `spontaneous` / `continuation-turn` channels and: - sends a dedicated question card via sendQuestionCard - intercepts the user's next typed reply for that chatId in handleMessage (before runningTasks / queue branches) - routes the parsed answer back via executor.resolveQuestion(), unblocking the hook and letting the SDK proceed - tears the card down to error state if the executor is evicted Multi-sub-question between-turn calls only surface the first question (rare; logged) — same single-question-at-a-time pattern as runOneTurn. Co-authored-by: Flood Sung <floodsung@xvirobotics.ai> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
SimonYeyi
pushed a commit
to SimonYeyi/metabot
that referenced
this pull request
May 26, 2026
…otics#275) `handleContinuationTurn` renders the agent's response to an SDK-injected `<task-notification>` (background bash return etc.) as a fresh streaming card. Before this fix, if the agent invoked AskUserQuestion *inside* that continuation turn, the stream processor would set `state.pendingQuestion` but the loop body just kept updating the main card with the question text inline — no dedicated question card, no reply routing. The user's typed reply was treated as a fresh user turn that immediately blocked for 6 minutes on the still-hanging hook. Fix: reuse the standalone between-turn question pipeline from xvirobotics#274. When the continuation stream yields `waiting_for_input` + `pendingQuestion`: - update the main card with a "_Waiting for your answer to the question card below…_" hint (drops `pendingQuestion`, mirrors the pattern in runOneTurn) - call `handleBetweenTurnQuestion` to surface a dedicated v1 question card and register the toolUseId for reply interception in handleMessage - track surfaced toolUseIds in a per-stream Set so we don't re-send the card on every subsequent delta while the same question is still waiting Same `executor.resolveQuestion()` path unblocks the hook; the continuation stream then continues normally. Co-authored-by: Flood Sung <floodsung@xvirobotics.ai> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
AskUserQuestionworks inside a user-initiated turn (the bridge seesstate.pendingQuestionand renders a dedicated question card with a typed-reply prompt). Between turns — teammate //goal/ continuation-burst follow-up — the PreToolUse hook inPersistentClaudeExecutorfires withactiveTurn === null, so there is no streamingCardStateto piggy-back on.between-turn-questionevent side-channel so the bridge can render a dedicated question card and route the user's next reply back through the existingexecutor.resolveQuestion()path.What changed
src/engines/claude/persistent-executor.tsparseAskUserQuestionInput()(mirrors the parse logic instream-processor.extractPendingQuestion) and exported aBetweenTurnQuestionEventpayload shape.askUserQuestionHook, whenthis.activeTurnis null, emitbetween-turn-questionafter the resolver has been registered inpendingQuestionResolvers(so a fast bridge reply can't race past it). The hook still awaits as before — emission is purely a side-channel.src/bridge/message-bridge.tspendingBetweenTurnQuestions: Map<chatId, { toolUseId, questions, cardMessageId }>.attachSpontaneousHandlersubscribes tobetween-turn-questionalongside the existingspontaneous/continuation-turnchannels.handleBetweenTurnQuestion()sends a fresh question card viasendQuestionCard(fallbacksendCard); single in-flight slot per chatId (older one is marked superseded). Multi-sub-question between-turn calls only surface the first question (rare; logged warning).tryHandleBetweenTurnQuestionReply()runs at the top ofhandleMessage(right after the/commandearly-return) — parses the typed reply, callsexecutor.resolveQuestion(), and updates the question card to a "complete" answered state. Falls back to error state if the executor is gone (e.g. evicted between question and reply).executor-removed(LRU eviction //reset), any pending between-turn question card is flushed to an error state so the user isn't left hanging.tests/between-turn-question.test.tsparseAskUserQuestionInput(well-formed / malformed / non-object input) and the executor hook (emits when no activeTurn; does NOT emit when one exists; resolver registered before event; abort cleans up).Test plan
Automated:
npm run build— clean.npx vitest run— 298/298 pass (was 291, +7 new).npm run lint— 0 errors (pre-existing warnings unrelated to this change).Manual:
/goaltask that triggersAskUserQuestionbetween user turns./reset— question card flips to "Question canceled — agent session ended." and a subsequent user message starts a new turn cleanly.🤖 Generated with Claude Code