Skip to content

fix(slackbot): prevent reply loops via event dedup and bot-origin filtering#62

Merged
rluisr merged 1 commit intomainfrom
fix/slackbot-reply-loop-prevention
Apr 30, 2026
Merged

fix(slackbot): prevent reply loops via event dedup and bot-origin filtering#62
rluisr merged 1 commit intomainfrom
fix/slackbot-reply-loop-prevention

Conversation

@rluisr
Copy link
Copy Markdown
Contributor

@rluisr rluisr commented Apr 30, 2026

Pull Request

Summary

Slackbot was replying repeatedly to the same user message. This PR closes two distinct loop sources at the transport layer and a third at the thread-context layer.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Performance improvement
  • Refactoring (no functional changes)

Changes Made

  • New internal/slackbot/dedup.go: TTL-bounded, size-bounded, concurrency-safe eventDedup keyed on Slack event_id (with channel:ts fallback) and on RTM client_msg_id (with channel:ts fallback).
  • SocketBot and Bot now reject events authored by any bot — BotID != "", missing user, our own botUserID, or our own bot_id — before they reach the processor. Processor.ProcessMessage keeps the same guard as a safety net.
  • Slack Events API retry deliveries are logged with retry_attempt and retry_reason for observability.
  • ThreadContextBuilder skips bot-authored history entries entirely instead of truncating them, so the bot's own past replies cannot reseed a new query. Tests renamed from TruncatesBotMessages to SkipsBotMessages and updated.
  • SocketBot now only forces MsgOptionTS(threadTS) when threading is enabled, matching the documented enable_thread option.

Motivation and Context

Two reproducible loops were observed in production:

  1. Slack redelivered the same event_id (Ack timeout, websocket reconnect, overlapping subscriptions). Each delivery produced a new reply.
  2. The bot's own reply was being pulled back into the thread history and reseeded the next query, so even after the user stopped, the bot kept replying to itself.

The fix is defense-in-depth: dedup at the wire, bot-origin filtering at the dispatcher and processor, and exclusion of bot history at the context builder.

Fixes #(issue)

How Has This Been Tested?

  • Unit tests (go test ./internal/slackbot/... -timeout 60s)
  • Integration tests
  • Manual testing with local setup
  • Tested with AWS services (S3 Vectors, OpenSearch, Bedrock)

New tests:

  • TestEventDedup_FirstSeenReturnsFalse / TestEventDedup_SecondSeenReturnsTrue
  • TestEventDedup_EmptyKeyIsNotDeduped
  • TestEventDedup_NilReceiverIsSafe
  • TestEventDedup_ExpiresAfterTTL
  • TestEventDedup_RespectsMaxSize
  • TestEventDedup_ConcurrentSafe

Updated tests:

  • TestThreadContextBuilder_BuildFormatsHistory now asserts bot lines are absent.
  • TestThreadContextBuilder_SkipsBotMessages (renamed from TruncatesBotMessages) covers BotID, SubType=bot_message, and Username=RAGent paths.

Test Configuration

  • Go version: 1.23
  • AWS Region: n/a
  • OpenSearch version (if applicable): n/a

Impact Analysis

Components Affected

  • CLI commands (cmd/)
  • Vectorization (internal/vectorizer/)
  • OpenSearch integration (internal/opensearch/)
  • S3 Vector operations (internal/s3vector/)
  • Slack bot (internal/slackbot/)
  • Bedrock embedding (internal/embedding/)
  • Configuration (internal/config/)

AWS Resources Impact

  • No AWS resource changes
  • S3 bucket operations
  • OpenSearch index structure
  • IAM permissions required
  • Bedrock model usage

Breaking Changes

  • None
  • Yes (describe below)

Behavioral note: bot-authored messages no longer appear in the formatted thread history that feeds the search query. This is intentional — including them was the loop amplifier — and the user-visible thread on Slack is unchanged.

Dependencies

  • No new dependencies
  • Dependencies added/updated (list below)

Documentation

  • README.md updated
  • CLAUDE.md updated
  • Inline code comments added/updated
  • API documentation updated
  • Configuration examples updated

Checklist

  • My code follows the project's style guidelines (go fmt ./... and go vet ./...)
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • I have checked my code for any security issues or exposed secrets
  • I have tested with the minimum supported Go version (1.23)
  • I have run go mod tidy to clean up dependencies

Performance Considerations

  • No performance impact
  • Performance improved (describe metrics)
  • Performance degraded but acceptable (explain trade-offs)

The dedup map is bounded at 4096 entries per process with a 5-minute TTL and opportunistic GC, so memory is O(maxSize). Lookups are O(1) under a single mutex.

Additional Notes

Dedup keys deliberately overlap (Events API uses event_id; RTM uses client_msg_id; both fall back to channel:ts) so any one transport quirk is caught by at least one layer.

Screenshots/Logs

n/a


プルリクエスト(日本語版)

概要

Slackbot が同一ユーザー発話に対して何度も返信してしまう不具合の修正。トランスポート層で重複イベントを抑止し、Bot 由来のイベント/履歴を除外することで二系統のループ要因を解消する。

変更の種類

  • バグ修正(既存機能を破壊しない問題の修正)
  • 新機能(既存機能を破壊しない機能の追加)
  • 破壊的変更(既存機能の動作に影響を与える修正や機能)
  • ドキュメント更新
  • パフォーマンス改善
  • リファクタリング(機能的変更なし)

実装された変更

  • internal/slackbot/dedup.go を新規追加。TTL とサイズで上限を持つ並行安全な eventDedupevent_id(フォールバック channel:ts)と RTM client_msg_id(フォールバック channel:ts)でキー化。
  • SocketBot / Bot が Bot 由来イベント(BotID あり、user 空、自身の botUserID、自身の bot_id)を Processor へ到達する前に破棄。Processor.ProcessMessage にも保険として同等のガードを残す。
  • Slack の retry 配信(retry_attempt / retry_reason)をログ出力し可観測性を確保。
  • ThreadContextBuilder は Bot 発話を切り詰めるのではなく履歴から完全に除外。テストを TruncatesBotMessagesSkipsBotMessages に改名・更新。
  • SocketBot はスレッド機能が有効な場合のみ MsgOptionTS(threadTS) を強制するよう修正(enable_thread 設定との整合)。

動機と背景

本番で再現した二つのループ要因:

  1. Slack が同一 event_id を再配信(Ack タイムアウト、WebSocket 再接続、サブスクリプション重複)するたびに新しい返信が生成されていた。
  2. Bot 自身の返信がスレッド履歴に取り込まれ、次回クエリの seed として再利用され、ユーザーが沈黙してもループが継続していた。

ワイヤ層での dedup、ディスパッチャと Processor での Bot 由来イベント除外、コンテキスト構築層での Bot 履歴除外、という多層防御で確実に断ち切る。

Fixes #(issue)

テスト方法

  • ユニットテスト(go test ./internal/slackbot/... -timeout 60s
  • 統合テスト
  • ローカル環境での手動テスト
  • AWSサービス(S3 Vectors、OpenSearch、Bedrock)でのテスト

テスト設定

  • Goバージョン: 1.23
  • AWSリージョン: n/a
  • OpenSearchバージョン(該当する場合): n/a

影響分析

影響を受けるコンポーネント

  • CLIコマンド(cmd/
  • ベクトル化(internal/vectorizer/
  • OpenSearch統合(internal/opensearch/
  • S3 Vector操作(internal/s3vector/
  • Slack bot(internal/slackbot/
  • Bedrock埋め込み(internal/embedding/
  • 設定(internal/config/

AWSリソースへの影響

  • AWSリソースの変更なし

破壊的変更

  • なし

挙動の補足: 検索クエリへ供給するスレッド履歴から Bot 発話を除外するように変更。Slack 上で見えるスレッド本文は変更しない。

依存関係

  • 新しい依存関係なし

ドキュメント

  • README.md更新
  • CLAUDE.md更新
  • インラインコードコメントの追加/更新
  • APIドキュメント更新
  • 設定例の更新

チェックリスト

  • コードがプロジェクトのスタイルガイドラインに従っている(go fmt ./...go vet ./...
  • 自分のコードをセルフレビューした
  • 理解が困難な領域にコメントを追加した
  • ドキュメントに対応する変更を行った
  • 変更によって新しい警告やエラーが生成されない
  • 修正が効果的であることまたは機能が動作することを証明するテストを追加した
  • 新しいテストと既存のユニットテストがローカルで成功する
  • 依存する変更がマージされ公開されている
  • セキュリティ問題や露出した秘密情報がないかコードをチェックした
  • サポートされる最小Goバージョン(1.23)でテストした
  • go mod tidyを実行して依存関係をクリーンアップした

パフォーマンスに関する考慮事項

  • パフォーマンスへの影響なし

dedup マップは最大 4096 件・TTL 5 分で機会的 GC を行うため、メモリは O(maxSize)。ルックアップは単一 mutex 下で O(1)。

追加ノート

dedup キーは意図的に重ね掛けしている(Events API は event_id、RTM は client_msg_id、いずれもフォールバックは channel:ts)。いずれかのトランスポート都合でキーが欠落しても他層で必ず検出できる。

スクリーンショット/ログ

n/a

…tering

Slackbot was responding repeatedly to the same user message in two cases:
- Slack redelivered the same Events API event (Ack timeout, ws reconnect)
- The bot's own reply, including a stray mention, retriggered processing
  through thread history reseeding

Defenses added:
- eventDedup: TTL-bounded, size-bounded, concurrent-safe seen-key tracker
- Skip events authored by any bot (BotID set, empty user, self user, or
  matching our own bot_id) at both RTM (Bot) and Socket Mode (SocketBot)
  transports, plus a Processor-level safety net
- Dedup keys: Events API event_id with channel:ts fallback for AppMention
  and DM message events; ClientMsgID with channel:ts fallback for RTM
- Log retry deliveries (retry_attempt, retry_reason) for diagnosability
- ThreadContextBuilder now skips bot-authored history entirely instead of
  truncating; tests updated to assert the new semantics
- SocketBot only forces thread TS when threading is enabled, matching the
  documented enable_thread option
@rluisr rluisr self-assigned this Apr 30, 2026
@rluisr rluisr deployed to e2e-test April 30, 2026 08:35 — with GitHub Actions Active
@rluisr rluisr merged commit dfb84f9 into main Apr 30, 2026
15 checks passed
@rluisr rluisr deleted the fix/slackbot-reply-loop-prevention branch April 30, 2026 08:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant