openclaw - ✅(Solved) Fix Slack react action does not check for already_reacted before calling reactions.add [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#58048Fetched 2026-04-08 01:54:34
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
cross-referenced ×1

Error Message

Slack API returns an already_reacted error when a duplicate reaction is attempted, causing the tool call to fail.

  1. Catch and silently ignore the already_reacted error (preferred — idempotent behavior), or if (err?.data?.error === 'already_reacted') return;

PR fix notes

PR #58242: fix(slack): treat duplicate reactions as success

Description (problem / solution / changelog)

Summary

  • Problem: Slack react actions bubbled Slack's already_reacted error when the same emoji was added twice to the same message.
  • Why it matters: retries and repeated tool calls should be idempotent instead of failing on a harmless duplicate reaction.
  • What changed: reactSlackMessage() now treats already_reacted as success, and a focused regression test locks that behavior in.
  • What did NOT change (scope boundary): this does not change reaction removal, Slack retry policy, or how unrelated Slack API errors are handled.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #58048
  • Related #58048
  • This PR fixes a bug or regression

Root Cause / Regression History (if applicable)

  • Root cause: reactSlackMessage() in extensions/slack/src/actions.ts forwarded client.reactions.add(...) errors directly, so Slack's duplicate-reaction response aborted the whole action.
  • Missing detection / guardrail: there was no direct unit test covering duplicate reaction handling at the Slack action helper boundary.
  • Prior context (git blame, prior PR, issue, or refactor if known): the helper path was introduced without a duplicate-reaction guard, while extensions/slack/src/monitor/message-handler/dispatch.ts already treated already_reacted as non-fatal for status reactions.
  • Why this regressed now: Unknown. The direct helper path appears to have always lacked the idempotent guard that the status-reaction path already had.
  • If unknown, what was ruled out: this is not caused by config parsing, retry policy, or message routing; the failure is the raw Slack API duplicate reaction error bubbling out of the helper.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/slack/src/actions.reactions.test.ts
  • Scenario the test should lock in: duplicate Slack reaction errors with data.error = "already_reacted" are ignored, while unrelated reaction errors still throw.
  • Why this is the smallest reliable guardrail: the bug happens inside the thin reactions.add wrapper, so a direct helper test covers the exact failing boundary without needing a live Slack workspace.
  • Existing test that already covers this (if any): extensions/slack/src/monitor/message-handler/dispatch.ts already handled already_reacted in the status-reaction path, but there was no helper-level regression test for direct Slack actions.
  • If no new test is added, why not: N/A.

User-visible / Behavior Changes

  • Re-adding the same Slack reaction to the same message no longer fails the Slack react action when Slack returns already_reacted.

Diagram (if applicable)

N/A.

Security Impact (required)

  • New permissions/capabilities? (Yes/No): No
  • Secrets/tokens handling changed? (Yes/No): No
  • New/changed network calls? (Yes/No): No
  • Command/tool execution surface changed? (Yes/No): No
  • Data access scope changed? (Yes/No): No
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22.14.0 via fnm
  • Model/provider: N/A
  • Integration/channel (if any): Slack action helper with mocked Slack WebClient
  • Relevant config (redacted): test token passed directly to the helper

Steps

  1. Call the Slack react action for a message.
  2. Call the same action again with the same emoji for the same Slack message.
  3. Observe Slack returning already_reacted on the duplicate add.

Expected

  • Duplicate reaction adds are treated as success.
  • Unrelated Slack API errors still fail normally.

Actual

  • Before this fix, the duplicate Slack reaction error bubbled out of reactSlackMessage() and failed the action.
  • After this fix, already_reacted returns successfully while unrelated errors still throw.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios: ran fnm exec --using 22.14.0 pnpm test -- extensions/slack/src/actions.reactions.test.ts and confirmed both the duplicate-reaction success path and the unrelated-error path.
  • Edge cases checked: :white_check_mark: is still normalized to white_check_mark before the Slack API call.
  • What you did not verify: a live Slack workspace, or a full green local pnpm check / pnpm build run.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No): Yes
  • Config/env changes? (Yes/No): No
  • Migration needed? (Yes/No): No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: the helper could accidentally hide a non-duplicate Slack error if the match is too broad.
    • Mitigation: the implementation checks Slack's structured data.error first, falls back only to the existing error-string shape, and the regression test confirms unrelated errors still throw.
  • Risk: local repo-wide verification is currently noisy because latest upstream/main has unrelated failures in extensions/qqbot/src/utils/audio-convert.ts, extensions/qqbot/src/utils/platform.ts, src/tasks/flow-registry.ts, and src/tasks/task-executor.ts.
    • Mitigation: this PR stays scoped to extensions/slack/src/actions.ts plus one targeted regression test, and the user-facing behavior is covered by the focused Slack unit test above.

Changed files

  • extensions/slack/src/actions.reactions.test.ts (added, +62/-0)
  • extensions/slack/src/actions.ts (modified, +34/-5)
  • extensions/slack/src/monitor/message-handler/dispatch.ts (modified, +0/-5)

Code Example

async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
    await (await getClient(opts)).reactions.add({
        channel: channelId,
        timestamp: messageId,
        name: normalizeEmoji(emoji)
    });
}

---

async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
    try {
        await (await getClient(opts)).reactions.add({
            channel: channelId,
            timestamp: messageId,
            name: normalizeEmoji(emoji)
        });
    } catch (err) {
        if (err?.data?.error === 'already_reacted') return;
        throw err;
    }
}
RAW_BUFFERClick to expand / collapse

Bug

reactSlackMessage in the Slack extension calls reactions.add directly without checking whether the bot has already reacted with the same emoji on that message.

Slack API returns an already_reacted error when a duplicate reaction is attempted, causing the tool call to fail.

Expected behavior

The react action should either:

  1. Catch and silently ignore the already_reacted error (preferred — idempotent behavior), or
  2. Call reactions.get first to check if the bot already reacted with that emoji before adding

Current behavior

async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
    await (await getClient(opts)).reactions.add({
        channel: channelId,
        timestamp: messageId,
        name: normalizeEmoji(emoji)
    });
}

No guard against duplicate reactions. If the agent tries to react with the same emoji twice (e.g. across retries or repeated tool calls), it throws.

Suggested fix

Wrap in try/catch and treat already_reacted as success:

async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
    try {
        await (await getClient(opts)).reactions.add({
            channel: channelId,
            timestamp: messageId,
            name: normalizeEmoji(emoji)
        });
    } catch (err) {
        if (err?.data?.error === 'already_reacted') return;
        throw err;
    }
}

Environment

  • OpenClaw latest (dist bundle)
  • Slack Web API via @slack/web-api

extent analysis

Fix Plan

To fix the issue, we will implement a try-catch block to catch and silently ignore the already_reacted error. Here are the steps:

  • Wrap the reactions.add call in a try-catch block
  • Check if the error is an already_reacted error
  • If it is, return from the function without throwing an error
  • If not, rethrow the error

Example Code

async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
    try {
        await (await getClient(opts)).reactions.add({
            channel: channelId,
            timestamp: messageId,
            name: normalizeEmoji(emoji)
        });
    } catch (err) {
        if (err?.data?.error === 'already_reacted') return;
        throw err;
    }
}

Alternatively, you can also call reactions.get first to check if the bot already reacted with that emoji before adding:

async function reactSlackMessage(channelId, messageId, emoji, opts = {}) {
    const client = await getClient(opts);
    const reaction = await client.reactions.get({
        channel: channelId,
        timestamp: messageId,
        full: true
    });
    if (reaction.message.reactions.some(r => r.name === normalizeEmoji(emoji) && r.users.includes((await client.auth.test()).user_id))) {
        return;
    }
    await client.reactions.add({
        channel: channelId,
        timestamp: messageId,
        name: normalizeEmoji(emoji)
    });
}

Verification

To verify that the fix worked, you can test the reactSlackMessage function by calling it twice with the same emoji and checking that it doesn't throw an error the second time. You can also check the Slack API logs to ensure that the already_reacted error is not being thrown.

Extra Tips

  • Make sure to handle other potential errors that may occur when calling the Slack API.
  • Consider adding logging to track when the already_reacted error is caught and ignored.
  • If you choose to implement the alternative solution using reactions.get, be aware that this will result in an additional API call, which may impact performance.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The react action should either:

  1. Catch and silently ignore the already_reacted error (preferred — idempotent behavior), or
  2. Call reactions.get first to check if the bot already reacted with that emoji before adding

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix Slack react action does not check for already_reacted before calling reactions.add [1 pull requests, 1 participants]