feat: rehydrate the clarifying-questions picker after a refresh (PR-3) - #1103
Merged
Merged
Conversation
The `user_question_required` event fires once and never re-streams, so a refresh mid-prompt orphaned the turn: picker gone, agent still paused waiting for an answer. `GET /messages` already replayed the breadcrumb PR-1 persists; this reads it back into the picker. Also fixes a pre-existing bug in the same area, found while building this and affecting OAuth and tool-approval prompts too. A breadcrumb outlives its `pausedTurn` snapshot when the user abandons a paused turn by simply typing something else — nothing cleared it, because the two `remove_pending_interrupts` call sites are resume cleanup and the explicit dismiss endpoint. Once rehydration exists that resurrects a prompt the user can no longer answer: the resume route 400s on an interrupt id the rebuilt agent never saw, so Submit becomes a guaranteed error. `clear_pending_interrupts` now runs beside `clear_paused_turn` at the head of every non-resume turn. It is a separate function rather than an addition to `clear_paused_turn` on purpose. That one clears only the snapshot — `test_paused_turn_independent_of_ pending_interrupts` pins it deliberately — and it also runs on the resume-success and expired-snapshot paths, where the narrower cleanup is already correct. The supersede policy belongs at the call site that owns it, next to `clear_interrupted_turn` and `clear_truncated_turn`. Question-shape validation is extracted to `validateUserQuestions`, shared by the SSE path and the reload path. They arrive by different transports — a parsed SSE frame and a JSON string out of DynamoDB — but must agree on what is renderable; two copies would drift, and the failure mode is a prompt that renders on one path and vanishes on the other. Verified against the running stack, both halves: refreshed mid-prompt and the picker came back with its questions, pager and Other field intact, then answering it resumed the turn through the snapshot-rebuild path with the selection reaching the model. Separately, abandoned a prompt by typing something else, refreshed, and got no stale picker. Co-Authored-By: Claude Opus 5 <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.
The
user_question_requiredevent fires once and never re-streams, so a refresh mid-prompt orphaned the turn — picker gone, agent still paused waiting for an answer.GET /messagesalready replayed the breadcrumb #1100 persists; this reads it back into the picker.Spec:
docs/specs/ask-user-question.mdAlso fixes a pre-existing bug, and this one bites all three interrupt flavors
A breadcrumb outlives its
pausedTurnsnapshot whenever a user abandons a paused turn by simply typing something else. Nothing cleared it — the tworemove_pending_interruptscall sites are resume cleanup and the explicit dismiss endpoint, neither of which a user reaches that way.That was survivable while nothing replayed them. Once rehydration exists it resurrects a prompt the user can no longer answer: the resume route 400s on an interrupt id the rebuilt agent never saw, so Submit becomes a guaranteed error.
clear_pending_interruptsnow runs besideclear_paused_turnat the head of every non-resume turn.It's a separate function rather than an addition to
clear_paused_turnon purpose. That one clears only the snapshot —test_paused_turn_independent_of_pending_interruptspins that deliberately — and it also runs on the resume-success and expired-snapshot paths where the narrower cleanup is already correct. The supersede policy belongs at the call site that owns it, next toclear_interrupted_turnandclear_truncated_turn.Checked all three
clear_paused_turncall sites: the new-turn one runs at the head of a turn (so it can't wipe a breadcrumb that turn later writes on its owndone), and the post-resume one is guarded byif not state_activated, so a turn that re-pauses keeps its fresh breadcrumb.Shared validation
Question-shape checks are extracted to
validateUserQuestions, used by both the SSE path and the reload path. They arrive by different transports — a parsed SSE frame and a JSON string out of DynamoDB — but must agree on what is renderable. Two hand-written copies would drift, and the failure mode is a prompt that renders on one path and vanishes on the other. (can_access_model/filter_accessible_modelsin CLAUDE.md is the cautionary tale.)Unparseable JSON and parsed-but-unrenderable payloads both drop the prompt rather than render it. A picker with no answerable option is a dead end — the user can neither answer nor dismiss it and the turn stays paused; rendering nothing at least leaves them able to retype.
Verification
Against the running stack, both halves:
Full SPA suite green (2,977; +27). Backend 3,073 across the touched suites.
What's left: PR-4, and it isn't polish
Measured against real Bedrock, the model reaches for the tool 4/4 as the only tool under the real default system prompt, 1–2/4 with
browse_webandcalculatorbeside it, and 0/4 through the full app. Temperature is not the variable. The feature is correct end to end but rarely fires on its own — a tool description alone may not be enough, and a system-prompt clause is the other lever.🤖 Generated with Claude Code