zeroclaw-labs/zeroclaw · error · anyhow::Error

approval prompts are not supported over interaction replies

Error message

approval prompts are not supported over interaction replies

What it means

The Discord channel refuses to deliver an approval prompt to a recipient that is an `interaction:{id}` reply-target sentinel (discord_interaction_reply_target). A deferred interaction reply has no channel of its own, and its single @original edit is reserved for the answer, so a buttoned approval prompt could never be posted there. request_approval_attributed bails before any REST call so the agent loop's deny-by-default approval policy applies instead of a doomed round-trip.

Source

Thrown at crates/zeroclaw-channels/src/discord/mod.rs:3861

        request: &ChannelApprovalRequest,
    ) -> anyhow::Result<Option<ChannelApprovalResponse>> {
        Ok(self
            .request_approval_attributed(recipient, request)
            .await?
            .map(|attributed| attributed.response))
    }

    async fn request_approval_attributed(
        &self,
        recipient: &str,
        request: &ChannelApprovalRequest,
    ) -> anyhow::Result<Option<zeroclaw_api::channel::AttributedApprovalResponse>> {
        // Approval prompts can't be delivered over a deferred interaction
        // reply (the sentinel is not a channel and the single @original
        // edit is reserved for the answer). Fail fast so the agent loop's
        // deny-by-default applies instead of a doomed REST round-trip.
        if parse_discord_interaction_target(recipient).is_some() {
            anyhow::bail!("approval prompts are not supported over interaction replies");
        }
        let token = crate::util::new_approval_token();

        let (tx, rx) = oneshot::channel();
        self.pending_approvals
            .lock()
            .await
            .insert(token.clone(), tx);

        // Strip thread suffix — approval message goes to the channel root.
        let channel_id = recipient.split(':').next().unwrap_or(recipient);

        let emitted = if self.slash_commands {
            self.send_buttoned_approval(channel_id, &token, request)
                .await
        } else {
            self.send_plaintext_approval(channel_id, &token, request)
                .await

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Pass the interaction's real channel id as the recipient instead of the `interaction:` sentinel (the approval path already splits any `:thread` suffix and posts to the channel root)
  2. Restructure the flow: answer the interaction first, then issue the approval prompt as a normal channel message
  3. If the approval cannot be rerouted, accept deny-by-default: catch this error, log it, and continue down the denied branch instead of retrying the same recipient

Example fix

// before
let recipient = discord_interaction_reply_target(&interaction_id); // "interaction:123..."
channel.request_approval(&recipient, &request).await?; // bails

// after
// route the prompt to the interaction's channel, not the sentinel
let recipient = interaction_channel_id.to_string(); // e.g. "987654321"
channel.request_approval(&recipient, &request).await?;
Defensive patterns

Strategy: validation

Validate before calling

// Rust — reject interaction sentinels before requesting approval
const INTERACTION_PREFIX: &str = "interaction:";
fn is_interaction_reply_target(recipient: &str) -> bool {
    match recipient.strip_prefix(INTERACTION_PREFIX) {
        Some(id) => !id.is_empty() && !id.contains(':'),
        None => false,
    }
}

if is_interaction_reply_target(recipient) {
    // deny-by-default: do not call the channel with this recipient
    return resolve_as_denied(request);
}
channel.request_approval(recipient, request).await?;

Try / catch

match channel.request_approval(recipient, &request).await {
    Ok(resp) => resp,
    Err(e) if e.to_string().contains("approval prompts are not supported over interaction replies") => {
        // expected for interaction targets: apply deny-by-default
        None
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: An agent flow triggers an approval gate (a tool call requiring approval) while the reply target is a deferred interaction: request_approval / request_approval_attributed is called with recipient = `interaction:{interaction_id}`, parse_discord_interaction_target accepts it, and the function bails immediately. Typical entry points: a slash-command invocation or button click that deferred its reply, then calls an approval-gated tool mid-interaction.

Common situations: A skill invoked through a Discord slash command calls a tool that requires approval before the interaction is answered; approval-gated operations on bots whose traffic arrives mostly via interactions; tests that capture a reply target from an interaction handler and reuse it for approvals.

Related errors


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