Skip to content

Approval buttons not appearing on progress message despite keyboard being rendered #104

@nathanschram

Description

Description

When Claude Code requests approval (ExitPlanMode), the progress message text updates to show the action but no inline keyboard buttons (Approve/Deny/Outline Plan) appear. The "Action required — approval needed" notification is sent, but the user has no way to approve or deny.

Impact

  • Critical: Claude Code subprocess blocks indefinitely waiting for a control response
  • User must use /cancel to recover (if they can find it)
  • The stall eventually triggers liveness warnings but the session is unrecoverable

Evidence from logs

  • render_progress.inline_keyboard_found action_id=claude.control.3 buttons=2 — renderer correctly generates approval keyboard
  • No telegram.api_error, outbox.op.failed, or transport errors anywhere
  • Subprocess stalled for 10+ minutes at control_request with idle_seconds=600

Root Cause (hypothesis)

The progress edit with the keyboard uses wait=False (fire-and-forget via outbox). The entire edit path from enqueue → outbox worker → Telegram API is invisible at info log level:

  1. ProgressEdits._run_loop calls transport.edit(wait=False) → returns None immediately
  2. TelegramTransport.edit() sees edited is None + not wait → returns original ref (optimistic success)
  3. last_rendered is updated as if the edit succeeded
  4. The actual edit executes asynchronously in the outbox worker — if it fails, nobody knows

Possible failure modes:

  • Telegram editMessageText returns "message is not modified" if only reply_markup changed but text/entities are identical to current state
  • Outbox coalescing drops the keyboard edit in favour of a subsequent non-keyboard render
  • Race between debounce timing and the control_request event

Observed in

  • Production v0.34.2, chat -5193338937 (lba-web), 2026-03-08 ~11:35 UTC
  • ExitPlanMode control request, 2 button rows (Approve/Deny + Outline Plan)

Possibly introduced by

  • 1908640 (render debouncing + non-blocking notifications) — changed notification to bg_tg.start_soon (async), debounce may delay keyboard edit
  • Outbox priority: SEND_PRIORITY=0 < EDIT_PRIORITY=2 means the notification send executes before the keyboard edit

Related

Files

  • src/untether/runner_bridge.pyProgressEdits._run_loop(), wait=False edit path
  • src/untether/telegram/bridge.pyTelegramTransport.edit(), optimistic return on wait=False
  • src/untether/telegram/outbox.pyexecute_op() silent error handling

Fix needed

  1. Add info-level logging when wait=False edits fail in the outbox worker
  2. Log the actual Telegram API response for keyboard edits at info level
  3. Consider using wait=True for keyboard transitions (approval buttons appearing/disappearing) since these are user-critical
  4. Add a retry mechanism for keyboard edits that fail

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingengine:claudeClaude Code CLI (Anthropic)

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions