Skip to content

feat: rehydrate the clarifying-questions picker after a refresh (PR-3) - #1103

Merged
philmerrell merged 1 commit into
developfrom
feature/ask-user-question-rehydration
Sep 14, 2026
Merged

philmerrell merged 1 commit into
developfrom
feature/ask-user-question-rehydration

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

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 #1100 persists; this reads it back into the picker.

Spec: docs/specs/ask-user-question.md

Also fixes a pre-existing bug, and this one bites all three interrupt flavors

A breadcrumb outlives its pausedTurn snapshot whenever a user abandons a paused turn by simply typing something else. Nothing cleared it — the two remove_pending_interrupts call 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_interrupts now runs beside clear_paused_turn at the head of every non-resume turn.

It's 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 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 to clear_interrupted_turn and clear_truncated_turn.

Checked all three clear_paused_turn call 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 own done), and the post-resume one is guarded by if 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_models in 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:

  • Rehydration — full page load mid-prompt brought the picker back with its questions, pager ("1 of 2") and Other field intact. Then answering the rehydrated prompt resumed the turn through the snapshot-rebuild path: selection reached the model, no error banner.
  • Abandonment — ignored a prompt, typed something else, refreshed: no stale picker, conversation renders normally.

Full SPA suite green (2,977; +27). Backend 3,073 across the touched suites.

ruff reports 9 findings in metadata.py / routes.py — all pre-existing (identical output on develop, verified by stashing) and none in the added code. Left alone as out of scope.

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_web and calculator beside 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

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>
@philmerrell
philmerrell merged commit 2507a5f into develop Sep 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant