Skip to content

feat(slack): add multiparty-mentions user-message gating mode - #1319

Closed
HSTsou wants to merge 2 commits into
openabdev:mainfrom
HSTsou:feat/slack-multiparty-mentions
Closed

feat(slack): add multiparty-mentions user-message gating mode#1319
HSTsou wants to merge 2 commits into
openabdev:mainfrom
HSTsou:feat/slack-multiparty-mentions

Conversation

@HSTsou

@HSTsou HSTsou commented Jul 7, 2026

Copy link
Copy Markdown

What problem does this solve?

Teams running a Slack bot face a gating dilemma in threads:

  • involved: great for 1:1 threads (conversation flows without re-@mentioning), but once other humans join the thread, the bot follows every message — side discussions between humans trigger the bot and burn tokens on messages not addressed to it.
  • mentions: safe in busy threads, but tedious in 1:1 — every follow-up needs an @mention.
  • multibot-mentions solves this adaptively for multiple bots, but not for multiple humans.

We hit this in production: users complained the bot replied to human-to-human side chatter in shared threads, but switching to mentions made 1:1 debugging sessions painful.

Discord Discussion URL: https://discord.com/channels/1491295327620169908/1523968250604556399

At a Glance

 message event (Slack Socket Mode)
        │
        ▼
 eager detection (no API calls)
   ├─ bot msg   → note_other_bot_in_thread()   (existing)
   └─ human msg → note_human_in_thread()        (new)
                    │ 2nd distinct human?
                    ▼
              multihuman_threads cache (positive-only, TTL-bounded)
        │
        ▼
 user-message gating: multiparty-mentions
   ├─ 1:1 thread (single human + bot, no other bot) → follow like `involved`
   └─ 2nd human OR other bot present               → require @mention
        ▲
        └─ bot_participated_in_thread() also derives multi-human from the
           fetched conversations.replies window (covers pre-restart history)

Prior Art & Industry Research

OpenClaw: group reply triggering is controlled by static mention gating — requireMention per group, with implicit mentions (reply/quote) and mentionPatterns. It's a per-group static switch: there is no mode that adapts within a thread based on how many humans are participating.

Hermes Agent: Telegram groups are mention-gated, with an "observed context" mode where unmentioned group messages are appended to the session transcript as context but only an @mention triggers a response. Again — no participant-count-adaptive gating.

Other references: openab's own multibot-mentions (this PR generalizes its "adaptive fallback" idea from bots to humans).

Proposed Solution

New allow_user_messages = "multiparty-mentions":

Thread state Behavior
Single human + this bot (1:1) Like involved — follows the thread without @mention
A second distinct human has posted Falls back to mentions
Another bot has posted Falls back to mentions (same as multibot-mentions)

Slack implementation mirrors the existing eager multibot machinery:

  • thread_first_human records the first human sender per thread; a second distinct sender marks the thread in the positive-only multihuman_threads cache — zero extra API calls on the hot path.
  • bot_participated_in_thread also derives multi-human from the fetched conversations.replies window (covers thread history predating the process) and now returns (involved, other_bot_present, multi_human).
  • Both caches use the existing enforce_cache_bounds TTL policy.

Discord/Feishu: multi-human detection not implemented yet — the mode behaves like multibot-mentions there (documented on the enum, in docs/config-reference.md, and chart values). Happy to follow up with Discord parity if there's interest.

Why this approach?

  • Adaptive beats static: a per-channel requireMention-style switch (OpenClaw's approach) can't distinguish a 1:1 debugging thread from a busy group thread in the same channel. Participant-count gating adapts per thread with no operator intervention.
  • Reuses proven machinery: the eager-detection + positive-cache + history-fetch-fallback pattern is exactly how multibot-mentions already works; this stays consistent with the codebase's existing mental model.
  • Known limitation: "multi-human" is irreversible per thread (like multibot) — if a second human posts once, the thread stays mention-gated for the cache TTL. We consider that the safe default.

Alternatives Considered

  • Static per-channel override (OpenClaw-style requireMention: false): rejected — doesn't adapt within a thread; operators would need per-thread toggles.
  • Observed-context mode (Hermes-style): different goal — it improves context quality but still requires a mention to act; doesn't remove the 1:1 friction.
  • Fetch-only detection (derive human count only in bot_participated_in_thread): rejected — the cached-involved early-return path skips the fetch, so a second human joining mid-conversation would go unnoticed. Eager per-event detection closes that gap.

Validation

  • cargo test -p openab-core --lib: 617 passed, 0 failed — includes 2 new should_process_user_message tests for the new mode and a config deserialization test covering both multiparty-mentions / multiparty_mentions spellings.
  • cargo check -p openab-core -p openab-gateway clean.
  • Exhaustive-match sites updated across discord.rs, feishu.rs (gateway has its own AllowUsers enum).
  • Docs updated: config.toml.example, charts/openab/values.yaml, docs/config-reference.md.

🤖 Generated with Claude Code

New `allow_user_messages = "multiparty-mentions"`: behaves like `involved`
while a thread is a 1:1 conversation (single human + this bot) — the bot
follows the whole thread without @mention. Once a second distinct human or
another bot posts in the thread, it falls back to `mentions`.

Slack implementation:
- Eager multi-human detection from message events (thread_first_human /
  multihuman_threads caches, mirroring eager multibot detection), so live
  traffic needs no extra API calls.
- bot_participated_in_thread now also derives multi-human from the fetched
  thread history (covers threads predating the process) and returns
  (involved, other_bot_present, multi_human).

Discord/Feishu: multi-human detection not implemented yet — the mode behaves
like multibot-mentions there (documented on the enum and in config docs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@HSTsou
HSTsou requested a review from thepagent as a code owner July 7, 2026 08:06
@openab-app openab-app Bot added closing-soon PR missing Discord Discussion URL — will auto-close in 24 hours. and removed closing-soon PR missing Discord Discussion URL — will auto-close in 24 hours. labels Jul 7, 2026
@chaodu-agent

chaodu-agent commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Important

CHANGES REQUESTED ⚠️ — Well-designed feature that correctly generalizes the existing multibot-mentions pattern, but has a logic gap in eager detection and needs test coverage before merge.

What This PR Does

Adds allow_user_messages = "multiparty-mentions" — an adaptive gating mode that follows 1:1 threads (single human + bot) without @mention, but requires @mention once a second human or another bot joins. Solves the problem of bots responding to human-to-human side chatter in shared threads.

How It Works

Eager per-event detection (note_human_in_thread) records the first human sender per thread; a second distinct sender marks the thread multi-human via a positive-only cache. Falls back to history-fetch detection on restart via bot_participated_in_thread (now returns a 3-tuple). On Discord/Feishu, behaves like multibot-mentions (multi-human detection is Slack-only).

Findings

# Severity Finding Location
1 🟡 app_mention events bypass multi-human detection — second human joining via @bot is not recorded slack.rs:882
2 🟡 thread_first_human cache can grow unbounded when all entries are fresh (only TTL eviction, no half-drop fallback) slack.rs:168-171
3 🟡 Missing unit tests for note_human_in_thread() eager detection and cache state transitions slack.rs
4 🟡 config-reference.md missing irreversibility note for multi-human detection docs/config-reference.md
5 🟡 Platform docs (discord.md, feishu.md, messaging.md) not updated with new mode docs/
6 🟡 Redundant lock acquisition on already-promoted multi-human threads (perf optimization opportunity) slack.rs:159
7 🟡 note_human_in_thread called unconditionally for all modes (even Mentions/Involved) — minor wasted work slack.rs:976-988
8 🟡 Multi-human state is in-memory only; restart recovery limited to 200-msg history window slack.rs (design decision)
9 🟡 values.yaml comment could clarify cross-platform fallback behavior charts/openab/values.yaml
10 🟢 3-tuple API change is well-contained; all 4 call sites updated correctly slack.rs:364-442
11 🟢 Fail-closed on all error paths — returns (false, false, false) consistently slack.rs:389-419
12 🟢 Exhaustive match coverage on Discord + Feishu with correct fallback semantics discord.rs, feishu.rs
13 🟢 Eager detection is O(1) per message, avoids API calls for live traffic slack.rs:151-183
14 🟢 Config deserialization handles both hyphen and underscore spellings config.rs:462
15 🟢 No new dependencies, no new auth surface, idempotent positive-only cache design
16 🟢 Eager + history-fallback dual-layer architecture mirrors existing multibot machinery slack.rs
Finding Details

🟡 F1: app_mention events bypass multi-human detection

note_human_in_thread() is only called in the "message" event branch. Slack's app_mention is a separate event type — if a second human joins by @mentioning the bot, they are never recorded in thread_first_human. Later unmentioned follow-ups from the first human will still be treated as 1:1.

Scenario: Thread T has first human U1. U2 joins via <@bot> (fires app_mention, not message). U1 sends unmentioned follow-up → bot responds (should require @mention).

Fix: Add note_human_in_thread() call in the app_mention branch for non-bot threaded senders.

🟡 F2: Unbounded cache growth in thread_first_human

if first.len() > PARTICIPATION_CACHE_MAX {
    let ttl = self.session_ttl;
    first.retain(|_, (_, ts)| ts.elapsed() < ttl);
}

If all entries are within TTL, retain() removes nothing and the map grows unbounded. The existing enforce_cache_bounds() has a secondary "drop oldest half" eviction — thread_first_human should use an equivalent strategy:

if first.len() > PARTICIPATION_CACHE_MAX {
    let ttl = self.session_ttl;
    first.retain(|_, (_, ts)| ts.elapsed() < ttl);
    if first.len() > PARTICIPATION_CACHE_MAX {
        let mut entries: Vec<_> = first.iter()
            .map(|(k, (_, ts))| (k.clone(), *ts)).collect();
        entries.sort_by_key(|(_, ts)| *ts);
        let evict_count = entries.len() / 2;
        for (key, _) in entries.into_iter().take(evict_count) {
            first.remove(&key);
        }
    }
}

🟡 F3: Missing unit tests

The new multi-human detection logic has complex state transitions but no dedicated unit tests. Recommended:

  • note_human_in_thread with same user (idempotent — no promotion)
  • note_human_in_thread with second distinct user (marks multi-human)
  • Cache eviction boundary behavior
  • bot_participated_in_thread history-fetch path with ≥2 human senders

🟡 F4: Docs gap — irreversibility note

config-reference.md should document: "Multi-human detection is irreversible per thread — once a second human posts, the thread stays mention-gated for the cache TTL."

🟡 F5: Platform docs not updated

docs/discord.md, docs/feishu.md, and docs/messaging.md still list only involved/mentions/multibot-mentions. Should add multiparty-mentions with fallback semantics note.

🟡 F6: Redundant lock on promoted threads

Once a thread is in multihuman_threads, subsequent messages still lock thread_first_human unnecessarily. An early-return check on multihuman_threads would skip both locks for already-promoted threads:

async fn note_human_in_thread(&self, thread_ts: &str, user_id: &str) {
    // Fast-path: already promoted, skip everything
    {
        let cache = self.multihuman_threads.lock().await;
        if cache.contains_key(thread_ts) { return; }
    }
    // ... rest of logic
}

🟡 F7: Unconditional note_human_in_thread calls

note_human_in_thread is called for every human thread message regardless of the configured mode. When allow_user_messages = "mentions" or "involved", the multi-human cache is never consulted. This is consistent with note_other_bot_in_thread (also unconditional) — but worth documenting the rationale (pre-populating cache for mode changes without restart).

🟡 F8: In-memory only multi-human state

Unlike multibot_threads (which persists to disk via MultibotCache), multihuman_threads and thread_first_human are in-memory only. On restart, multi-human state is recovered via bot_participated_in_thread's history fetch — but this only covers the last ~200 messages. Threads older than that window lose their multi-human state until a second human posts again.

Recommendation: Document this explicitly as a known limitation in the PR. If the 200-msg window is acceptable (likely is for most workspaces), add a code comment. If not, consider disk persistence similar to MultibotCache.

Baseline Check
  • PR opened: 2026-07-07
  • Main already has: multibot-mentions mode with similar positive-cache + eager-detection pattern
  • Net-new value: Multi-human adaptive gating (Slack-only detection), generalizing the multibot approach to humans
What's Good (🟢)
  • Reuses proven multibot architecture — no new design patterns to learn
  • Fail-closed on every error path (API failure, missing bot ID, malformed response)
  • Eager detection avoids API calls on the hot path (O(1) hash map ops)
  • Exhaustive match coverage across all platforms with appropriate fallback
  • Config handles both spelling conventions (hyphen/underscore)
  • No new dependencies or auth surface
  • Clear documentation of Slack-only scope in all config files
  • Dual-layer design (eager + history-fallback) mirrors multibot machinery exactly

5️⃣ Three Reasons We Might Not Need This PR

  1. Complexity vs. frequencymultibot-mentions already handles the most common multi-agent scenario. Multi-human side chatter in bot threads may be rare enough that the added cache complexity (2 new Mutex maps, eager tracking per message) isn't justified.
  2. Platform inconsistency — Slack-only detection means operators get different behavior across platforms for the same config value. This could confuse users and complicate troubleshooting.
  3. Configuration cognitive load — A fourth allow_user_messages mode adds decision burden. The incremental UX improvement over multibot-mentions (only relevant when humans chat in bot threads) may not justify the config complexity.

@thepagent

thepagent commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Discord Discussion URL: https://discord.com/channels/1491295327620169908/1523968250604556399

incorrect link. Setting closing-soon if not fixed.

@thepagent thepagent added the closing-soon PR missing Discord Discussion URL — will auto-close in 24 hours. label Jul 8, 2026
@howie

howie commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Mob code review — PR #1319 feat(slack): multiparty-mentions

Mob review by 3 independent LLM reviewers (Claude 4-subagent · Codex · agy), run as R1 independent review → R2 cross-debate → aggregation. These are suggestions — the call on what to apply is yours. No Critical/blocking issue survived cross-verification (one claimed Critical was empirically refuted, see the bottom). Overall this is a well-structured feature with fail-closed error handling; the notes below are mostly about edge-case gating robustness, test coverage, and docs.

Important suggestions

  1. multi_human can fail-open in long/existing/post-restart threadscrates/openab-core/src/slack.rs (bot_participated_in_thread early-return ~L394, history count ~L449-463, gating ~L1155-1174)
    All three reviewers converged here. When cached_involved is true, the function early-returns cached_multihuman and never reconciles with the current sender or a fresh fetch. Combined with (a) the 200-message conversations.replies window, (b) thread_first_human not being seeded when a fetch finds exactly 1 human, and (c) parent-message authors never being eagerly recorded (parent events lack thread_ts), a genuinely multi-human thread can be answered without an @mention. The live same-session path is backstopped by the eager note_human_in_thread call, so this is an edge case rather than the common path — but it is reachable. Possible fixes (any/all): when cached_involved && !cached_multihuman, still fetch and reconcile; seed thread_first_human when a fetch finds 1 human; merge the live sender before deciding; fall back to event["ts"] to record the parent author. (One reviewer, agy, rates this Critical; the other two rate it Important — see "Where reviewers disagreed".)

  2. The new Slack multi-human logic has no behavioral unit testscrates/openab-core/src/slack.rs
    The added tests cover enum deserialization and Discord predicates (where the mode is a deliberate no-op). The actual state machine — note_human_in_thread transitions (first / same / second distinct human), the 3-tuple bot_participated_in_thread, and the gating truth table — is untested, though the in-file #[tokio::test] harness already supports it with no network. Consider extracting a pure multiparty_should_process(involved, other_bot, multi_human, mentions_bot) (mirroring the tested Discord predicate) and table-testing it, plus direct tests for note_human_in_thread and the cached-involved early return.

  3. Distinct-human filter doesn't exclude the bot's own user idcrates/openab-core/src/slack.rs:449-453
    The human count excludes bot_id/bot_message but not messages where user == bot_id. Standard Slack app-posted messages carry bot_id and are already filtered, so this is defensive hardening rather than a live bug — but a one-line .filter(|&uid| uid != bot_id) would make it robust and matches the existing bot_posted check that keys on user == bot_id.

  4. Discord/Feishu silently degrade multiparty-mentions to multibot-mentionscrates/openab-core/src/discord.rs:777-800, crates/openab-gateway/src/adapters/feishu.rs:2470-2487
    Both accept the value but implement no multi-human detection; only a doc-comment notes it. An operator picking the mode specifically to stop the bot butting into multi-human conversations gets silently weaker gating off Slack. Consider an info!/warn! at adapter startup when MultipartyMentions is set on a non-Slack platform.

  5. thread_first_human grows unbounded under loadcrates/openab-core/src/slack.rs:159-171
    Unlike its sibling caches (which force-evict the oldest half via enforce_cache_bounds), this map only prunes expired entries, and it records every human-active thread (not just involved ones). A busy workspace with more than PARTICIPATION_CACHE_MAX fresh threads within one session_ttl can grow without bound. Consider force-evicting oldest entries after the TTL retain if still over cap.

Minor suggestions (nits)

  1. Stale/contradictory Returns doc-commentcrates/openab-core/src/slack.rs:363, 365
    L363 still says Returns (involved, other_bot_present) (2-tuple) and L365 says "returns (false, false) on API error", but the function now returns a 3-tuple (false, false, false). Suggest deleting L363 and updating L365.

  2. Feishu "multiparty-mentions" (dash) match arm is unreachablecrates/openab-gateway/src/adapters/feishu.rs:217, 222
    .replace('-', "_") at L217 normalizes before the match, so the dash literal at L222 can never match (only "multiparty_mentions" does). Works fine today; dropping the dash literal would match the sibling arms and avoid the "dead code" impression.

  3. config.toml.example Slack section lists multiparty-mentions but never explains itconfig.toml.example:31-35
    charts/openab/values.yaml:341-342 explains it; consider mirroring that bullet here.

  4. config.toml.example Discord section advertises multiparty-mentions with no caveatconfig.toml.example:16
    Consider noting it behaves like multibot-mentions on Discord (no multi-human detection), consistent with the config.rs enum doc-comment.

  5. "irreversible" doc-comment overstates durabilitycrates/openab-core/src/slack.rs:79-80
    multihuman_threads is in-memory only (unlike disk-persisted multibot_cache) and is lost on restart/TTL expiry. Consider "session-scoped, rebuilt from history" — or persist it to match multibot.

Where reviewers disagreed (your call)

  • Claimed deserialization panic — refuted, not a finding. One reviewer (agy) initially flagged a [Critical] that the hyphenated "multiparty-mentions" would panic the added test and fail CI. This was empirically checked and refuted: config.rs:465 normalizes with .to_lowercase().replace('-', "_") before matching, and running the exact test gives test config::tests::allow_users_deserializes_multiparty_mentions ... ok. The other two reviewers disagreed and agy retracted it in cross-debate. Noted only for transparency.
  • Severity of suggestion perf: cache dependency build layer in Dockerfile #1 (fail-open): Important vs Critical. agy argues Critical (an un-mentioned reply in a real multi-human thread is a gating bypass); Codex and Claude argue Important (reachable only on the edge — >200-msg threads, cache eviction, or parent-only authors with participation already cached; the common path is backstopped). A definitive Critical rating would need a live-Slack runtime reproduction, which this review couldn't construct — you're best placed to judge against your traffic patterns.

Generated by a multi-model mob review (/mob-code-review-only). Base was pinned to the upstream/main merge-base (325→7 files) after an initial fork-origin mismatch. Suggestions only — no code was modified.

…he, tests, docs

- F1: app_mention events now feed note_human_in_thread (a second human
  joining via @bot was previously invisible to multiparty-mentions).
- F2: thread_first_human uses the same two-stage eviction as
  enforce_cache_bounds (TTL retain, then oldest-half drop) so the map
  stays bounded even when all entries are fresh.
- F6: fast path — already-promoted multi-human threads return after a
  single cache check, skipping first-human bookkeeping.
- F7: documented why eager detection runs unconditionally for all modes.
- F8: documented the in-memory-only design and ~200-message history
  recovery window on the cache field.
- F3: 4 unit tests (idempotency, promotion, fast path, bounded eviction).
- F4/F5/F9: irreversibility note in config-reference; multiparty-mentions
  added to messaging/discord/feishu docs with Slack-only fallback note;
  chart values comment clarifies cross-platform behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@openab-app openab-app Bot removed the closing-soon PR missing Discord Discussion URL — will auto-close in 24 hours. label Jul 8, 2026
@HSTsou

HSTsou commented Jul 8, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review @chaodu-agent — all findings addressed in 1b5e40c:

  • F1 (app_mention bypass): app_mention branch now calls note_human_in_thread for non-bot threaded senders — a second human joining via @bot is detected. ✅
  • F2 (unbounded cache): thread_first_human now uses the same two-stage eviction as enforce_cache_bounds (TTL retain → oldest-half drop). ✅
  • F3 (tests): 4 new unit tests — same-user idempotency, second-user promotion, promoted fast-path, bounded eviction with all-fresh entries. cargo test -p openab-core --lib: 621 passed. ✅
  • F4: irreversibility note added to config-reference.md. ✅
  • F5: multiparty-mentions documented in messaging.md, discord.md, feishu.md with the Slack-only detection / multibot-mentions fallback note. ✅
  • F6 (redundant lock): fast-path early return on already-promoted threads. ✅
  • F7: rationale comment added (unconditional eager detection mirrors multibot; O(1) cost, cache stays warm across mode changes). ✅
  • F8: in-memory-only design + ~200-message history recovery window documented on the cache field as an accepted limitation. ✅
  • F9: chart values comment clarifies cross-platform fallback. ✅

On the "three reasons we might not need this": fair challenges. Our production data point — two bots in shared team channels where humans regularly discuss inside bot threads — is exactly the case multibot-mentions can't cover, and mentions makes 1:1 debugging threads tedious. The platform inconsistency is documented at every config surface; happy to follow up with Discord parity if maintainers want it before or after merge.

🤖 Addressed by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants