Skip to content

fix(feishu): add early event-level dedup to prevent duplicate message processing#37528

Closed
zhuowater wants to merge 1 commit intoopenclaw:mainfrom
zhuowater:fix/feishu-webhook-event-dedup
Closed

fix(feishu): add early event-level dedup to prevent duplicate message processing#37528
zhuowater wants to merge 1 commit intoopenclaw:mainfrom
zhuowater:fix/feishu-webhook-event-dedup

Conversation

@zhuowater
Copy link
Copy Markdown

Problem

Fixes #37477

Feishu webhook retries and WebSocket reconnect replays can deliver the same im.message.receive_v1 event multiple times. The existing messageId dedup in handleFeishuMessage (bot.ts) runs downstream after the inbound debouncer, so two concurrent dispatches of the same event can both enter the pipeline before either records the messageId — causing duplicate replies.

Fix

Add a synchronous in-memory dedup check at the EventDispatcher handler level (monitor.account.ts) using message_id as key with a 5-minute TTL and 2000-entry cap. This catches duplicates immediately when they arrive from the Lark SDK, before they enter the inbound debouncer or processing queue.

The dedup cache uses the same createDedupeCache utility already used by the persistent dedup module, keeping the implementation consistent.

The downstream persistent dedup in bot.ts is intentionally preserved — it guards against cross-restart duplicate processing which the in-memory cache cannot cover.

Changes

  • monitor.account.ts: Import createDedupeCache from openclaw/plugin-sdk/feishu. Create a per-account event dedup cache before registering event handlers. In the im.message.receive_v1 handler, check message_id against the cache before passing to the debouncer.

Test plan

  • Send a message to Feishu bot in DM — should receive exactly one reply
  • Simulate webhook retry by sending the same event payload twice — second should be logged as duplicate and dropped
  • Verify existing behavior: messages from different users/chats still process normally
  • Verify persistent dedup still works across gateway restarts
  • Run pnpm test -- extensions/feishu — existing tests should pass

… processing

Feishu webhook retries and WebSocket reconnect replays can deliver the
same event multiple times. The existing messageId dedup in
handleFeishuMessage runs downstream after the debouncer, so two
concurrent dispatches of the same event can both enter the pipeline
before either records the messageId.

Add a synchronous in-memory dedup check at the EventDispatcher handler
level using message_id as key with a 5-minute TTL. This catches
duplicates immediately when they arrive, before they enter the
inbound debouncer or processing queue.

The downstream persistent dedup in bot.ts is preserved for
cross-restart protection.

Closes openclaw#37477
@openclaw-barnacle openclaw-barnacle bot added channel: feishu Channel integration: feishu size: XS labels Mar 6, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 42e6b3c553

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const messageId = event.message?.message_id?.trim();
if (messageId) {
const eventKey = `${accountId}:evt:${messageId}`;
if (!eventDedup.check(eventKey)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Invert event dedupe guard to process first delivery

createDedupeCache.check returns false for a new key and true only after the key has already been seen (src/infra/dedupe.ts:57-67), but this handler currently returns early when check is false. In normal Feishu im.message.receive_v1 traffic (where message_id is present), that means the first delivery of each message is dropped and only later retries/replays can proceed, effectively suppressing legitimate inbound messages.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds an early in-memory dedup cache to the Feishu im.message.receive_v1 event handler to prevent duplicate processing from webhook retries and WebSocket replay — the right idea and the right place to put it. However, there is a critical inverted-logic bug: the guard condition uses !eventDedup.check(eventKey), but createDedupeCache.check() returns false on the first visit and true on subsequent (duplicate) visits. As written, every original message is silently dropped and every duplicate is processed, which is exactly the opposite of the fix's intent and would cause a regression worse than the original problem.

The change is otherwise well-structured (correct helper, correct TTL/size, correct placement), so the fix is straightforward: remove the ! negation on line 392.

Confidence Score: 1/5

  • Not safe to merge — the inverted dedup condition will drop every legitimate first message and let all retries/replays through, causing a regression worse than the bug being fixed.
  • A critical logic inversion (! operator on line 392) makes the dedup check do the opposite of what is intended: original messages are discarded and duplicates are allowed through. The change is otherwise well-structured (correct helper, correct key, correct TTL/size), so the fix is a single-character removal. However, without fixing this bug, the PR introduces a severe regression that completely inverts the dedup behavior.
  • extensions/feishu/src/monitor.account.ts — line 392 dedup guard condition

Last reviewed commit: 42e6b3c

Comment on lines +392 to +395
if (!eventDedup.check(eventKey)) {
log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createDedupeCache.check(key) returns false on the first encounter (recording the key) and true on subsequent (duplicate) encounters. The negation !eventDedup.check(eventKey) inverts this:

  • First message arrives → check returns false!false === true → the message is dropped
  • Duplicate arrives → check returns true!true === false → the duplicate passes through

This is precisely backwards from the intended behaviour — every original message will be silently discarded and every retry/replay will be processed. The ! must be removed:

Suggested change
if (!eventDedup.check(eventKey)) {
log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);
return;
}
if (eventDedup.check(eventKey)) {
log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);
return;
}

The existing tryRecordMessage in dedup.ts wraps check the same way — return !memoryDedupe.check(messageId) — and returns true for new messages. The direct check call here needs the negation removed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/monitor.account.ts
Line: 392-395

Comment:
`createDedupeCache.check(key)` returns `false` on the **first** encounter (recording the key) and `true` on **subsequent** (duplicate) encounters. The negation `!eventDedup.check(eventKey)` inverts this:

- First message arrives → `check` returns `false``!false === true` → the message is **dropped**- Duplicate arrives → `check` returns `true``!true === false` → the duplicate **passes through** ❌

This is precisely backwards from the intended behaviour — every original message will be silently discarded and every retry/replay will be processed. The `!` must be removed:

```suggestion
        if (eventDedup.check(eventKey)) {
          log(`feishu[${accountId}]: dropping duplicate event for message ${messageId}`);
          return;
        }
```

The existing `tryRecordMessage` in `dedup.ts` wraps `check` the same way — `return !memoryDedupe.check(messageId)` — and returns `true` for new messages. The direct `check` call here needs the negation removed.

How can I resolve this? If you propose a fix, please make it concise.

@vincentkoc
Copy link
Copy Markdown
Member

Closing this as superseded by #43762.

Both PRs patch the same Feishu im.message.receive_v1 handler with the same early event-level dedupe strategy. The difference is important: createDedupeCache.check() returns true for an already-seen key, so this PR's if (!eventDedup.check(eventKey)) guard is inverted for that API shape.

#43762 carries the same fix path with the corrected predicate and the same monitor.account.ts surface, so keeping both open would split review across overlapping implementations.

@vincentkoc vincentkoc closed this Mar 12, 2026
@Takhoffman
Copy link
Copy Markdown
Contributor

This earlier PR targeted the same Feishu early-event dedupe race that was ultimately landed in #43762.

Keeping this closed as superseded by the merged fix.

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

Labels

channel: feishu Channel integration: feishu size: XS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Feishu channel duplicates messages - event deduplication needed

3 participants