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)
.awaitView on GitHub (pinned to 88bb9c8533)
Solutions
- 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)
- Restructure the flow: answer the interaction first, then issue the approval prompt as a normal channel message
- 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
- Never hand an `interaction:{id}` sentinel to approval-prompt APIs; approval prompts need a real channel id
- Store the channel id alongside the interaction id when deferring a reply so later flows can route prompts to the channel
- Treat approval delivery failures as deny-by-default in the agent loop rather than retrying the same recipient
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
- ACP request_permission timed out after {:?}
- ACP returned unknown permission optionId: {other}
- ACP returned unexpected permission outcome: {other}
- interaction defer failed ({status}): {err}
- modal custom_id exceeds Discord's 100-char limit; cannot ope
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/d5238d5f7a9cd883.
Report an issue: GitHub.