{"record":{"id":"d5238d5f7a9cd883","repo":"zeroclaw-labs/zeroclaw","slug":"approval-prompts-are-not-supported-over-interactio","errorCode":null,"errorMessage":"approval prompts are not supported over interaction replies","messagePattern":"approval prompts are not supported over interaction replies","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/discord/mod.rs","lineNumber":3861,"sourceCode":"        request: &ChannelApprovalRequest,\n    ) -> anyhow::Result<Option<ChannelApprovalResponse>> {\n        Ok(self\n            .request_approval_attributed(recipient, request)\n            .await?\n            .map(|attributed| attributed.response))\n    }\n\n    async fn request_approval_attributed(\n        &self,\n        recipient: &str,\n        request: &ChannelApprovalRequest,\n    ) -> anyhow::Result<Option<zeroclaw_api::channel::AttributedApprovalResponse>> {\n        // Approval prompts can't be delivered over a deferred interaction\n        // reply (the sentinel is not a channel and the single @original\n        // edit is reserved for the answer). Fail fast so the agent loop's\n        // deny-by-default applies instead of a doomed REST round-trip.\n        if parse_discord_interaction_target(recipient).is_some() {\n            anyhow::bail!(\"approval prompts are not supported over interaction replies\");\n        }\n        let token = crate::util::new_approval_token();\n\n        let (tx, rx) = oneshot::channel();\n        self.pending_approvals\n            .lock()\n            .await\n            .insert(token.clone(), tx);\n\n        // Strip thread suffix — approval message goes to the channel root.\n        let channel_id = recipient.split(':').next().unwrap_or(recipient);\n\n        let emitted = if self.slash_commands {\n            self.send_buttoned_approval(channel_id, &token, request)\n                .await\n        } else {\n            self.send_plaintext_approval(channel_id, &token, request)\n                .await","sourceCodeStart":3843,"sourceCodeEnd":3879,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/discord/mod.rs#L3843-L3879","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nlet recipient = discord_interaction_reply_target(&interaction_id); // \"interaction:123...\"\nchannel.request_approval(&recipient, &request).await?; // bails\n\n// after\n// route the prompt to the interaction's channel, not the sentinel\nlet recipient = interaction_channel_id.to_string(); // e.g. \"987654321\"\nchannel.request_approval(&recipient, &request).await?;","handlingStrategy":"validation","validationCode":"// Rust — reject interaction sentinels before requesting approval\nconst INTERACTION_PREFIX: &str = \"interaction:\";\nfn is_interaction_reply_target(recipient: &str) -> bool {\n    match recipient.strip_prefix(INTERACTION_PREFIX) {\n        Some(id) => !id.is_empty() && !id.contains(':'),\n        None => false,\n    }\n}\n\nif is_interaction_reply_target(recipient) {\n    // deny-by-default: do not call the channel with this recipient\n    return resolve_as_denied(request);\n}\nchannel.request_approval(recipient, request).await?;","typeGuard":null,"tryCatchPattern":"match channel.request_approval(recipient, &request).await {\n    Ok(resp) => resp,\n    Err(e) if e.to_string().contains(\"approval prompts are not supported over interaction replies\") => {\n        // expected for interaction targets: apply deny-by-default\n        None\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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"],"tags":["discord","approval","interaction","slash-command","fail-fast"],"backgroundTag":"channel-capability-mismatch","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}