Skip to content

[slack] Missing envelope_id deduplication causes double-processing during connection rotation #1103

@pulpcorn

Description

@pulpcorn

Summary

The Slack Socket Mode adapter (crates/openfang-channels/src/slack.rs) does not deduplicate incoming events by envelope_id. Slack intentionally delivers the same event to multiple active WebSocket connections for reliability and expects apps to deduplicate. This results in the agent processing the same user message twice, sending duplicate replies to the channel.

Slack docs reference

From the official Slack Socket Mode guidance:

"Your app must implement deduplication based on envelope_id to avoid double-processing during connection rotation."

Socket Mode regularly rotates connections (disconnect: warning ~ every hour) and, during the grace window, an event can flow through both the old and the new connection.

Reproduction

  1. Configure a Slack app with Socket Mode + an OpenFang Slack channel.
  2. Send a message that mentions the bot.
  3. Trigger a connection rotation, either by waiting for Slack's periodic disconnect: warning, or by restarting openfang.service mid-conversation.
  4. Observe the bot posting the same response twice.

Relevant server logs (redacted):

Apr 22 12:10:38 INFO Slack disconnect request: warning
Apr 22 12:10:38 WARN Slack: reconnecting in 1s
Apr 22 12:10:39 INFO Connecting to Slack Socket Mode...
Apr 22 12:10:40 INFO Slack Socket Mode connected
Apr 22 12:10:40 INFO Slack disconnect request: too_many_websockets
Apr 22 12:10:40 WARN Slack: reconnecting in 1s
Apr 22 12:10:41 INFO Connecting to Slack Socket Mode...
Apr 22 12:10:41 INFO Slack Socket Mode connected
Apr 22 12:10:41 INFO Slack disconnect request: too_many_websockets
...

In the user-facing Slack channel, the same @bot find emails: ... prompt produced two identical responses posted back-to-back.

Root cause

In crates/openfang-channels/src/slack.rs around lines 275–309, the events_api branch:

  1. Reads envelope_id from payload
  2. Acknowledges the envelope
  3. Forwards the event to the internal mpsc channel for agent dispatch

There is no check that envelope_id has been processed already. Grep confirms no other occurrence of envelope_id outside this ack path, and no dedup cache exists anywhere in the Slack adapter.

Proposed fix

Introduce a bounded TTL cache keyed by envelope_id and short-circuit before forwarding:

use dashmap::DashMap;
use std::time::{Duration, Instant};
use once_cell::sync::Lazy;

// 60-second TTL is well above the typical rotation overlap window (< 10s)
static RECENT_ENVELOPES: Lazy<DashMap<String, Instant>> = Lazy::new(DashMap::new);
const ENVELOPE_TTL: Duration = Duration::from_secs(60);

// ...inside the "events_api" branch, right after ack:
if !envelope_id.is_empty() {
    // Opportunistic GC of expired entries (cap growth)
    if RECENT_ENVELOPES.len() > 10_000 {
        let now = Instant::now();
        RECENT_ENVELOPES.retain(|_, ts| now.duration_since(*ts) < ENVELOPE_TTL);
    }
    if let Some(prev) = RECENT_ENVELOPES.get(envelope_id) {
        if prev.elapsed() < ENVELOPE_TTL {
            debug!("Slack: skipping duplicate envelope_id {envelope_id}");
            continue;
        }
    }
    RECENT_ENVELOPES.insert(envelope_id.to_string(), Instant::now());
}

The ack must still be sent (so Slack does not retry), but the event should not be forwarded to the mpsc channel if already seen.

Impact

Any OpenFang deployment using Slack Socket Mode with active usage periodically produces duplicate bot responses. It also amplifies any LLM/API cost and rate-limit pressure (each duplicate causes a full agent run + tool call).

Environment

  • OpenFang: main branch (reproduced on build from 2026-04-21)
  • Slack Socket Mode: xapp-* app-level token
  • Adapter: crates/openfang-channels/src/slack.rs

Happy to submit a PR with the dedup patch if desired.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions