Skip to content

bug: AskUserQuestion final-question inline keyboard not removed after answers sent (multi-Q flow) #550

@nathanschram

Description

Symptom

In an AskUserQuestion multi-question sequential flow, after the user clicks an option for the final question, the inline keyboard on that question message is not removed. Buttons remain visible and clickable. The structured answer DOES reach Claude correctly (control_response.sent approved=True) and Claude continues the task; the bug is purely a UX cleanup gap.

Subsequent clicks on the now-stale buttons fire ask_question.flow_missing action=opt warnings (no-op — flow state already cleaned up on the success path).

Reproduction (nsd VPS, v0.35.3rc17, 2026-05-17)

  • Host: nsd
  • Chat: -1003933918343 (NSD Marketing group)
  • Project: nsd-marketing
  • Session: edd9e51e-e62a-4e68-aa64-39e4a538bbc7
  • user_msg_id: 350
  • request_id: 63a75c8f-c1ca-4ba6-821c-02a5a69b0ba7

Claude issued an AskUserQuestion with 2 sequential questions. User clicked option buttons on Q1 then Q2:

12:50:34Z  callback.dispatch  command=aq   chat_id=-1003933918343        (Q1 click)
12:50:35Z  callback.answered  command=aq early=True latency_ms=217.6
12:50:36Z  callback.dispatch  command=aq                                  (Q2 click — first)
12:50:36Z  callback.answered  command=aq early=True latency_ms=187.6
12:50:36Z  render_progress.inline_keyboard_found  action_id=claude.control.2  buttons=5
12:51:06Z  render_progress.inline_keyboard_found  action_id=claude.control.2  buttons=5  ← still rendered
12:51:09Z  callback.dispatch  command=aq                                  (Q2 click — repeat)
12:51:09Z  control_response.sent  approved=True  request_id=63a75c8f  session=edd9e51e   ← answer flushed
12:51:09Z  catalog.refresh_sent  ...  (Claude continues task)
12:52:03Z  callback.dispatch  command=aq                                  (Q2 button still visible — click)
12:52:03Z  ask_question.flow_missing  action=opt                           ← warning: flow gone
12:52:17Z  render_progress.inline_keyboard_found  action_id=claude.control.3  buttons=2   (next Q rendered)

After 12:51:09 the keyboard should be stripped from the original Q2 message. It isn't.

Root cause

src/untether/telegram/commands/ask_question.py:137-152 — the "all questions answered" branch of AskQuestionCommand.handle():

else:
    # All questions answered — send structured response
    success = await answer_ask_question_with_options(flow.request_id)
    if success:
        answer_lines = []
        for question, answer in flow.answers.items():
            answer_lines.append(f\"Q: {question}\\nA: {answer}\")
        answers_summary = \"\\n\\n\".join(answer_lines)
        return CommandResult(
            text=f\"Answers sent:\\n\\n{answers_summary}\",
            notify=True,
        )
    return CommandResult(
        text=\"Failed to send answerssession may have ended\",
        notify=True,
    )

Compare to the "more questions" branch directly above (lines 124-136), which correctly calls await ctx.executor.edit(ctx.message, msg) to transition the message from Q1 → Q2 (which re-renders with fresh buttons). The final-answer branch sends a new "Answers sent: …" message via CommandResult.text but never touches the original question message — so its inline keyboard persists.

Impact

  • Severity: minor. The flow works; user just sees stale buttons.
  • User confusion: "did my answer go through?" — leads to redundant clicks (firing ask_question.flow_missing warnings).
  • Visual debt: Cleanup is asymmetric between the intermediate-question transition (clean) and the final-question completion (stale buttons).
  • Possible interaction with ENH-PATCH: callback-answer latency escalates 6-10× under rapid-click clusters (220ms baseline → 1.4-2.9s on 2nd/3rd click) #546 (rapid-click callback latency): In this run, the user clicked the Q2 button twice (12:50:36 + 12:51:09, 33s apart) before control_response.sent fired. Once the keyboard is removed promptly, this kind of repeated clicking should go away naturally.

Fix sketch

In the final-answer branch (after success = await answer_ask_question_with_options(...)), strip the keyboard from the original message before constructing the CommandResult:

# After successful answer send, remove the keyboard from the question message
if success:
    cleared = RenderedMessage(
        text=ctx.message.text or format_question_message(flow),  # preserve text
        extra={\"parse_mode\": \"HTML\", \"reply_markup\": {\"inline_keyboard\": []}},
    )
    try:
        await ctx.executor.edit(ctx.message, cleared)
    except Exception as exc:  # noqa: BLE001
        logger.warning(\"ask_question.keyboard_clear_failed\", exc=str(exc))
    # ... existing answer_lines / answers_summary code follows

Alternative: register the question message as ephemeral via register_ephemeral_message() in runner_bridge.py at the point the Ask flow is shown, so it's auto-deleted when the run completes via ProgressEdits.delete_ephemeral(). The register_ephemeral_message pattern is the standard Untether convention for approval-related messages (per .claude/rules/telegram-transport.md § Ephemeral messages).

Acceptance criteria

  • After the final question in a multi-Q AskUserQuestion flow is answered, the original question message no longer carries an inline keyboard.
  • The ask_question.flow_missing action=opt warning no longer fires from user re-clicks on a completed flow (because the buttons are gone).
  • Unit test in tests/test_ask_user_question.py covering: 2-question flow, click Q1 then Q2, assert ctx.executor.edit called with empty/cleared reply_markup on the final answer.

Related


Discovered: 2026-05-17 during direct user-reported observation (NSD Marketing chat). Logs cross-checked on host nsd via journalctl --user -u untether.
Version: v0.35.3rc17 (all 4 fleet hosts).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingengine:claudeClaude Code CLI (Anthropic)priority: lowseverity:minorSmall UX gap, edge case, cosmetic issue; doesn't block any workflow

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions