{"record":{"id":"c52718f4bb1d00ea","repo":"zeroclaw-labs/zeroclaw","slug":"slack-reactions-remove-failed-err","errorCode":null,"errorMessage":"Slack reactions.remove failed: {err}","messagePattern":"Slack reactions\\.remove failed: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-channels/src/slack.rs","lineNumber":5563,"sourceCode":"            .send()\n            .await?;\n\n        let status = resp.status();\n        let text = resp.text().await.unwrap_or_default();\n\n        if !status.is_success() {\n            let sanitized = zeroclaw_providers::sanitize_api_error(&text);\n            anyhow::bail!(\"Slack reactions.remove failed ({status}): {sanitized}\");\n        }\n\n        let parsed: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();\n        if parsed.get(\"ok\") == Some(&serde_json::Value::Bool(false)) {\n            let err = parsed\n                .get(\"error\")\n                .and_then(|e| e.as_str())\n                .unwrap_or(\"unknown\");\n            if err != \"no_reaction\" {\n                anyhow::bail!(\"Slack reactions.remove failed: {err}\");\n            }\n        }\n\n        Ok(())\n    }\n\n    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {\n        // Cache the bot user id on the struct so `self_handle` (sync,\n        // called by the orchestrator's self-loop guard on every inbound)\n        // resolves without an additional `auth.test` round-trip.\n        self.cache_bot_user_id().await;\n        let bot_user_id = self.get_bot_user_id().await.unwrap_or_default();\n        let scoped_channels = self.scoped_channel_ids();\n        if self.configured_app_token().is_some() {\n            ::zeroclaw_log::record!(\n                INFO,\n                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note),\n                \"channel listening in Socket Mode\"","sourceCodeStart":5545,"sourceCodeEnd":5581,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/slack.rs#L5545-L5581","documentation":"Thrown when the Slack Web API `reactions.remove` call returns `ok: false` with an error code other than `no_reaction`. The library already treats `no_reaction` as success (the emoji is already gone), so any other Slack error code — message_not_found, too_old, cant_remove_reaction, not_authed — propagates with the raw code embedded in the message.","triggerScenarios":"Calling reactions.remove with a `timestamp` for a deleted message or one older than Slack's reaction window (message_not_found, too_old); a bot token missing the reactions:write scope (cant_remove_reaction, not_allowed); an invalid or revoked token (not_authed); a `channel` ID the bot cannot see (channel_not_found).","commonSituations":"Bot token regenerated and the app not reinstalled with the new scopes; retrying reaction cleanup after the target message was deleted; races where a parallel process removed the reaction; classic-bot vs workspace-app permission mismatches.","solutions":["Read the Slack error code embedded after 'failed:' — message_not_found/to_old mean the target is gone and can be treated as benign.","Verify the bot token includes `reactions:write` and reinstall the Slack app if scopes changed.","Confirm the `channel` + `timestamp` pair passed to reactions.remove comes from the same event payload that reported the reaction.","On cleanup/dedup paths, catch the error and downgrade to a warning, mirroring the library's own no_reaction tolerance."],"exampleFix":"// before\nchannel.remove_reaction(&channel_id, &ts, &emoji).await?;\n\n// after\nif let Err(e) = channel.remove_reaction(&channel_id, &ts, &emoji).await {\n    let msg = e.to_string();\n    if msg.contains(\"no_reaction\") || msg.contains(\"message_not_found\") || msg.contains(\"too_old\") {\n        tracing::warn!(\"reaction already gone: {e}\");\n    } else {\n        return Err(e);\n    }\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn slack_reaction_error_code(err: &anyhow::Error) -> Option<&str> {\n    err.to_string()\n        .strip_prefix(\"Slack reactions.remove failed: \")\n}","tryCatchPattern":"if let Err(e) = channel.remove_reaction(ch, ts, emoji).await {\n    match slack_reaction_error_code(&e) {\n        Some(\"no_reaction\") | Some(\"message_not_found\") | Some(\"too_old\") => {\n            tracing::warn!(\"reaction target gone: {e}\");\n        }\n        _ => return Err(e),\n    }\n}","preventionTips":["Cache reaction ts/channel from the event payload and remove promptly, before messages can be deleted or age out.","Keep the reactions:write scope in the app manifest and re-verify after token rotation.","Treat reaction removal as best-effort: warn instead of propagating on cleanup paths."],"tags":["slack","reactions","bot-api","anyhow","scopes"],"backgroundTag":"slack-web-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}