Skip to content

bug(slack): app_mention re-fires for every edit state → duplicate agent turns (phantom turns) #1103

Description

@dogzzdogzz

Summary

The Slack adapter spawns a full agent turn for every app_mention event, with no dedup by message ts. A single logical mention, however, commonly arrives as several app_mention events — Slack re-delivers app_mention as a mention-bearing message moves through edit states, and openab's own native streaming drives a reply through several message states (chat.startStream → N×appendStreamchat.stopStream → final chat.update). The result is multiple duplicate agent invocations ("phantom turns") for one user message, each of which also forces a reply into the thread.

Environment

  • openab main (src/slack.rs app_mention handler)
  • Slack adapter, Socket Mode
  • Multi-agent thread configuration (≥2 bots), assistant_mode default

Observed behavior

In a multi-agent workflow thread, a single exchange of 2 real user messages produced 5 agent turns. The extra turns:

  • share the same sender and (channel, ts);
  • carry token counts that track the message's growth as it settles (e.g. 75 → 90 tokens), i.e. they are successive edit states of the same message;
  • queue behind one another (dispatch wait climbing ~12s → 24s → 32s as duplicates pile up);
  • each emit a forced reply (often a no-op), which is pure thread noise.

One dispatch was even observed acting on a mid-stream fragment whose text was just <@U…> - — the mention captured before it had settled.

Root cause (src/slack.rs)

The app_mention branch of the Socket Mode loop spawns handle_message unconditionally for each event:

"app_mention" => {
    // ... bot gating ...
    let event = event.clone();
    // ...
    tokio::spawn(async move {
        handle_message(&event, /* ... */).await;
    });
}

There is no per-message dedup, so every edit-state re-fire of the same (channel, ts) becomes its own turn. The turn-boundary batching dispatcher (docs/adr/turn-boundary-batching.md) coalesces distinct messages that arrive during an in-flight turn, but it does not dedupe repeated edit states of the same message.

Reproduction

  1. A Slack workspace with a bot in a multi-agent thread (≥2 bots), assistant_mode default (native streaming).
  2. @mention the bot with a message and let the reply stream (and/or edit the mention message once after sending).
  3. Observe multiple handle_message dispatches / agent turns for the single mention, several of which emit a no-op reply.

Impact

  • Duplicate agent invocations — wasted LLM cost and latency — for one user message.
  • Thread noise: each phantom turn forces a reply.
  • Especially visible in multi-agent threads, where native streaming is disabled (post+edit) and a reply produces several edit states.

Suggested fix

Debounce app_mention per (channel, ts): hold only the latest event state, dispatch once after a short quiet window (each new state pushes the deadline), and only if the settled state's fingerprint (text + attached file IDs) differs from what was last dispatched for that message. Trailing-edge, so the turn acts on the settled message rather than a mid-stream fragment; identical re-fires drop, material edits re-dispatch (fail-open). A PR implementing this follows.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions