Skip to content

fix(messaging): dedupe short-window getUpdates inbound replays - #240

Open
ericcaiwx-star wants to merge 3 commits into
Tencent:mainfrom
ericcaiwx-star:fix/inbound-message-dedupe
Open

ericcaiwx-star wants to merge 3 commits into
Tencent:mainfrom
ericcaiwx-star:fix/inbound-message-dedupe

Conversation

@ericcaiwx-star

@ericcaiwx-star ericcaiwx-star commented Jul 30, 2026

Copy link
Copy Markdown

Summary

  • Claim a stable inbound key (message_idclient_idseq → body fingerprint) at the top of processOneMessage via OpenClaw createClaimableDedupe, then commit a disk tombstone under $OPENCLAW_STATE_DIR/openclaw-weixin/replay-dedupe/ so at-least-once getUpdates deliveries do not run the AI pipeline twice.
  • Covers ~1s iLink replays and longer redeliveries (e.g. 30–50 min after a stuck long turn), including across process restart.
  • Derive stable MessageSid from the same key when transport ids are present.
  • Tests for key selection, claim/commit/TTL/release; CHANGELOG (EN/ZH).

Fixes #239
Related: openclaw/openclaw#116723

Scope / deploy assumption

  • 24h replay-dedupe / tombstone window — same transport id / claim key. Not a content-dedupe window: a user intentionally re-sending the same text with a new message_id is processed.
  • Disk-backed claim survives restart (single host / shared OPENCLAW_STATE_DIR). Multi-replica gateways without shared state still need a shared store or full channel ingress drain — out of scope here; this matches the transitional createClaimableDedupe pattern used by other channels (Discord / Mattermost / Nextcloud Talk / Feishu).

message_id on the wire

iLink GetUpdatesResp.msgs[] items are WeixinMessage; README documents message_id as the unique message id. Production debug already logs msgId / seq.

{
  "seq": 1234567890,
  "message_id": 9876543210,
  "from_user_id": "<redacted>",
  "to_user_id": "<redacted>",
  "create_time_ms": 1720000000000,
  "item_list": [{ "type": 1, "text_item": { "text": "<redacted>" } }],
  "context_token": "<redacted>"
}

Test plan

  • npx vitest run src/messaging/inbound-dedupe.test.ts src/messaging/inbound.test.ts
  • npx tsc --noEmit
  • Maintainer: smoke personal WeChat — send one text, confirm single agent turn / single reply
  • Long-turn / late redelivery: same message_id within 24h is dropped; intentional re-send (new id) still processes
  • Restart gateway and confirm tombstone file under ~/.openclaw/openclaw-weixin/replay-dedupe/ still suppresses replay

Claim message_id/client_id/seq (or body fingerprint) before side effects so at-least-once iLink long-polls do not run the AI pipeline twice. Derive MessageSid from the same stable key when present.

Fixes Tencent#239

Co-authored-by: Cursor <cursoragent@cursor.com>
Spell out that the 5-minute TTL is for getUpdates replays (not
swallowing intentional user re-sends), and that the in-memory claim
is process-local / single-instance only.

Co-authored-by: Cursor <cursoragent@cursor.com>
Upgrade the getUpdates replay guard from a process-local Map to
OpenClaw createClaimableDedupe with a 24h disk tombstone under
OPENCLAW_STATE_DIR, covering ~1s iLink doubles and longer redeliveries
after stuck long turns without swallowing intentional re-sends.

Co-authored-by: Cursor <cursoragent@cursor.com>
@NewFuture

Copy link
Copy Markdown

Thanks for putting together a thorough replay-deduplication patch. The same at-least-once delivery gap exists in the current community release, and we would prefer an author-led port so the implementation context and authorship stay with you.

If you are interested, we would be glad to review a focused PR against:

https://github.com/NewFuture/openclaw-weixin

For the current codebase, please adapt the claim/commit flow to the ordinary and approval admission lanes, confirm compatibility with the supported OpenClaw persistent-dedupe API, and retain account-scoped storage. Regression coverage for restart persistence, cross-account isolation, claim release after failure/abort, media, and slash-command delivery would be especially helpful.

Contribution guide:
https://github.com/NewFuture/openclaw-weixin/blob/main/CONTRIBUTING.md

For transparency, NewFuture/openclaw-weixin is a community-maintained distribution, not the official Tencent/Weixin release.

@ericcaiwx-star

ericcaiwx-star commented Aug 3, 2026

Copy link
Copy Markdown
Author

Community port opened as invited: NewFuture/openclaw-weixin#41

Adapted for ordinary/approval admission lanes and OpenClaw 2026.7.1 createClaimableDedupe plugin-state API. This Tencent PR remains for upstream review.

NewFuture pushed a commit to NewFuture/openclaw-weixin that referenced this pull request Aug 4, 2026
…nes (#41)

* fix(messaging): persist getUpdates replay claims on both admission lanes

Claim/commit OpenClaw createClaimableDedupe tombstones (pluginId
openclaw-weixin, account namespace, 24h) inside ordinary and approval
lanes so at-least-once iLink replays do not run twice across restart.
Stable MessageSid follows the same transport key when present.

Port of Tencent/openclaw-weixin#240 for the community distribution.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(messaging): keep replay dedupe compatible with OpenClaw 2026.6.1

Use the resolveFilePath createClaimableDedupe shape so min-host CI can
typecheck, and soften Windows temp-dir cleanup when the state DB stays open.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(messaging): reclaim after in-flight release and redact dedupe logs

Wait on ClaimableDedupe inflight pending so a replay is dropped only after
the owner commits; reclaim if the owner releases. Duplicate logs keep only
non-sensitive identity kinds (mid/cid/seq/body).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(messaging): keep lanes free during inflight replay waits

Observe ClaimableDedupe inflight pending outside ordinary/approval lanes
and re-enqueue only after owner release; wrap all post-admission work in
the claim lifecycle; prefer item msg_id digests and never key by sender
alone for empty-body media.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
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.

[Bug] getUpdates at-least-once delivery double-invokes AI pipeline (≈1s duplicate user messages)

2 participants