zeroclaw-labs/zeroclaw · error

Telegram sendMessage (approval) failed after fallback

Error message

Telegram sendMessage (approval) failed after fallback

What it means

Defensive backstop at the end of the Telegram approval send flow: it fires when the send_ok flag is still false after the HTML sendMessage and the plain-text fallback both failed. In the current code every failure branch above either bails with the more specific 'Telegram sendMessage (approval) failed ({status}): {err}' message (error 280) or returns the reqwest transport error directly, so this branch is effectively unreachable — every path that sets nothing returns true. Seeing this message alone means the failure fell through the specific branches without capturing a status.

Source

Thrown at crates/zeroclaw-channels/src/telegram.rs:4699

                        let err = r.text().await.unwrap_or_default();
                        self.pending_approvals.lock().await.remove(&approval_id);
                        anyhow::bail!("Telegram sendMessage (approval) failed ({status}): {err}");
                    }
                    Err(e) => {
                        self.pending_approvals.lock().await.remove(&approval_id);
                        return Err(e.into());
                    }
                }
            }
            Err(e) => {
                self.pending_approvals.lock().await.remove(&approval_id);
                return Err(e.into());
            }
        };

        if !send_ok {
            self.pending_approvals.lock().await.remove(&approval_id);
            anyhow::bail!("Telegram sendMessage (approval) failed after fallback");
        }

        // Wait for the user to tap a button. Timeout is configurable via
        // `channels.telegram.approval_timeout_secs` (default 120s).
        let result =
            match tokio::time::timeout(Duration::from_secs(self.approval_timeout_secs), rx).await {
                Ok(Ok(response)) => Some(
                    zeroclaw_api::channel::AttributedApprovalResponse::operator(response),
                ),
                Ok(Err(_)) => {
                    // Sender dropped — clean up and deny. Nobody tapped.
                    self.pending_approvals.lock().await.remove(&approval_id);
                    Some(
                        zeroclaw_api::channel::AttributedApprovalResponse::from_runtime(
                            ChannelApprovalResponse::Deny,
                            zeroclaw_api::channel::ApprovalSource::Unreachable,
                        ),
                    )

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Treat it exactly like error 280: check bot token validity, chat_id, and rate limits
  2. Grep the logs for the preceding WARN 'Telegram sendMessage (approval) with HTML failed' — it carries the actual status and body of the first attempt
  3. If you hit this exact message with no preceding specific error, report it as a control-flow bug in the approval send path in crates/zeroclaw-channels/src/telegram.rs
Defensive patterns

Strategy: try-catch

Try / catch

// Same handling as the specific sendMessage failure: catch, inspect,
// and deny the pending tool call so the agent does not hang.
if let Err(e) = approval_request.await {
    tracing::warn!("approval delivery failed: {e}; defaulting to deny");
    return ChannelApprovalResponse::Deny;
}

Prevention

When it happens

Trigger: Both the HTML-formatted and plain-text sendMessage attempts for an approval prompt fail without a transport error, and neither specific bail branch fires. Practically unreachable today: the fallback's non-success branch bails with the detailed error 280 first, and transport errors return early.

Common situations: Same conditions as error 280 (bad token, unknown chat, rate limit) persisting across both attempts; or a future refactor that breaks the match chain so failures fall through with send_ok false.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/a23e9714576835e5. Report an issue: GitHub.