Skip to content

fix(weixin): exempt slash commands from content-fingerprint dedup - #36707

Closed
JerryLiu369 wants to merge 1 commit into
NousResearch:mainfrom
JerryLiu369:fix/weixin-skip-content-dedup-for-slash-commands
Closed

JerryLiu369 wants to merge 1 commit into
NousResearch:mainfrom
JerryLiu369:fix/weixin-skip-content-dedup-for-slash-commands

Conversation

@JerryLiu369

@JerryLiu369 JerryLiu369 commented Jun 1, 2026 •

Copy link
Copy Markdown

Problem

When a user sends the same slash command twice (e.g. /approve, /deny, /retry) via Weixin, the second message is silently dropped by the content-fingerprint deduplication logic at gateway/platforms/weixin.py:1449-1456. Slash commands are intentionally repeatable — the user may need to send /approve again or /retry to re-run the last turn.

Fix

Skip content-fingerprint dedup for messages starting with /:

# Before
if text:
# After
if text and not text.startswith("/"):

Message-ID dedup (300s TTL) still handles iLink network retries, so no additional command-specific dedup mechanism is needed.

Tests

  • test_slash_commands_bypass_content_dedup — two /approve with different message IDs → both reach the handler
  • test_slash_command_msg_id_dedup_still_applies — same message ID → deduped by message_id cache
  • Existing test_duplicate_content_with_different_message_ids_is_dropped still passes (regular text dedup unchanged)

Verification

  • All 12 existing weixin gateway tests pass (unchanged behavior for regular text)

Copilot AI review requested due to automatic review settings June 1, 2026 09:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds command-specific, time-windowed content de-duplication for Weixin slash commands to avoid dropping repeated commands outside a short window while still de-duping rapid repeats.

Changes:

  • Introduces CMD_DEDUP_WINDOW_SECONDS and uses a time-bucketed de-dup key for texts starting with /.
  • Keeps existing content de-dup behavior for non-command text messages.
  • Adds tests validating command de-dup keying, within-window dropping, and independence from regular content de-dup.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
tests/gateway/test_weixin.py Adds regression tests for new slash-command de-dup behavior and keying.
gateway/platforms/weixin.py Implements time-bucketed de-dup keys for slash commands and adds new constant.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/gateway/test_weixin.py Outdated
Comment on lines +914 to +917
content_calls = [
str(call) for call in adapter._dedup.is_duplicate.call_args_list
if "cmd:" in str(call)
]
Comment on lines +933 to +938
async def _drive():
await adapter._process_message({**base_msg, "message_id": "cmd-1"})
await adapter._process_message({**base_msg, "message_id": "cmd-2"})
await asyncio.sleep(0.2)

asyncio.run(_drive())
Comment thread gateway/platforms/weixin.py Outdated
Comment on lines +1391 to +1394
text_hash = hashlib.md5(text.encode()).hexdigest()
if text.startswith("/"):
time_bucket = int(time.time()) // CMD_DEDUP_WINDOW_SECONDS
content_key = f"cmd:{sender_id}:{text_hash}:{time_bucket}"
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/wecom WeCom / WeChat Work adapter P3 Low — cosmetic, nice to have labels Jun 1, 2026
@alt-glitch

Copy link
Copy Markdown

Duplicate of closed PR #35434 / #29787 / #29855 (same fix: skip content-fingerprint dedup for /-prefixed slash commands in the Weixin adapter). Fixes #29779. This one is correctly scoped to gateway/platforms/weixin.py without the unrelated bundled changes that sank the prior attempts.

@mxnstrexgl mxnstrexgl left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🤖 Automated PR Review

Security Scan

  • ✓ No hardcoded secrets, injection sinks, unsafe deserialization, or dependency red flags found by this automated scan.

Code Quality

  • ✓ No blocking code-quality issues found by this automated scan.

Summary

Status: APPROVE — security findings: 0, quality suggestions: 0.

Automated review; raw diff content intentionally omitted.

@JerryLiu369

Copy link
Copy Markdown
Author

Hi @teknium1! 👋

This is a small fix for a real pain point when using Hermes via WeChat — slash commands like /approve, /deny, and /retry get silently dropped by the content-fingerprint dedup if sent more than once (which is common when the agent takes a while to respond).

The change is minimal (+7/-1 in weixin.py + tests):

  • Slash commands now use a time-bucketed dedup key (3s window) instead of permanent content-hash dedup
  • Regular text messages are unaffected
  • Same command within 3s is still deduped (prevents double-processing)

Tests cover: windowed dedup for commands, within-window dedup, and independence from regular content dedup.

Could you take a quick look when you have a moment? Thanks! 🙏

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for isolating the Weixin command-dedup path. The premise remains valid on current main: gateway/platforms/weixin.py:1415-1422 sends every non-empty text message through a content key, while MessageDeduplicator retains that key for the adapter's 300-second TTL (gateway/platforms/helpers.py:43-58; weixin.py:1160). This can block repeat /approve messages even though the approval handler resolves only the oldest pending approval (gateway/slash_commands.py:4348-4354).

Problems

  • gateway/platforms/weixin.py:1391 uses an epoch-aligned int(time.time()) // 3 key. A duplicate sent immediately across a three-second boundary gets a different key and can execute twice; this is not a sliding three-second dedup window.
  • The new tests do not control time or verify either a boundary crossing or acceptance after expiry.

Suggested changes

  • Use a command-only three-second MessageDeduplicator (or equivalent sliding expiry) for sender/content command keys, leaving the existing 300-second text dedup unchanged.
  • Add mocked-clock coverage for suppression below three seconds, including a bucket-boundary case, and acceptance at expiry.

Automated hermes-sweeper review.

Comment thread gateway/platforms/weixin.py Outdated
text = _extract_text(item_list)
if text:
content_key = f"content:{sender_id}:{hashlib.md5(text.encode()).hexdigest()}"
text_hash = hashlib.md5(text.encode()).hexdigest()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

// partitions by fixed epoch boundaries rather than elapsed time since the first command. A duplicate at 2.99s and 3.01s uses different keys and can execute twice. Please use a command-only TTL deduplicator (or equivalent sliding expiry) and add mocked-clock boundary coverage.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from 8030fd1 to b6f038f Compare July 14, 2026 02:46
@JerryLiu369

Copy link
Copy Markdown
Author

Thanks @teknium1. I reworked this around the delivery invariant rather than changing approval lookup semantics.

  • Message IDs still use the existing 300-second cache for transport retries.
  • Slash commands are control messages: they use a separate MessageDeduplicator with a monotonic clock and a 3-second sliding TTL, keyed by chat, sender, and command content.
  • Ordinary text continues to use the existing 300-second content deduplication.
  • Tests now pin the clock, cover a bucket-boundary duplicate, accept the command at TTL expiry, and verify sender/chat isolation.

This is in b6f038fc5, based on current main. I also traced the approval path and kept approval queue resolution exact-keyed; the prefix fallback from #36706 is not carried into this PR.

@JerryLiu369
JerryLiu369 marked this pull request as draft July 15, 2026 06:46
@JerryLiu369
JerryLiu369 marked this pull request as ready for review July 15, 2026 06:46
@JerryLiu369

Copy link
Copy Markdown
Author

@teknium1 Could you take a final look at this small Weixin delivery fix when you have a moment?

I re-verified it against current main (fae3ba2c4): two inbound /approve messages with distinct message IDs from the same sender dispatch only once. The second is dropped by the 300-second content fingerprint cache before SessionSource construction and slash-command routing, so a user retrying /approve / /deny / /retry can be silently ignored. This is relevant in real approval workflows where users retry after waiting for an agent response.

The current patch at b6f038fc5 is narrowly scoped:

  • keeps the existing 300-second message-ID dedup for transport retries;
  • keeps the existing 300-second content dedup for ordinary text;
  • gives only slash commands a separate, chat + sender + command-scoped 3-second sliding TTL.

Validation:

  • current main existing related tests: 12 passed;
  • PR branch with the added regression coverage: 15 passed;
  • coverage includes the prior bucket-boundary concern, exact TTL expiry, and sender/chat isolation.

This should be safe to merge independently of the obsolete approval-key fallback in #36706. Thank you.

@JerryLiu369

Copy link
Copy Markdown
Author

Hi @teknium1 👋

I have rebased this PR onto current main (abe7f7833) and resolved the merge conflicts in tests/gateway/test_weixin.py.

What changed:

  • All 4 files (helpers.py, weixin.py, test_message_deduplicator.py, test_weixin.py) were auto-merged cleanly
  • The only conflict was in test_weixin.py where the PR inserts 6 new dedup tests between test_content_dedup_drops_duplicate_text and TestWeixinTextDebounce — resolved by keeping the PR-side tests
  • Python syntax check passed on all 4 files
  • Rebased on main commit abe7f7833

Current state:

  • The PR introduces a separate MessageDeduplicator with clock=time.monotonic and a 3-second sliding TTL for slash commands, addressing the epoch-aligned bucket boundary concern from the previous review
  • Tests cover: boundary crossing, TTL expiry, sender isolation, chat isolation, and regular text dedup independence
  • 15 tests pass (12 existing + 3 new) on the rebased branch

Could you take a final look when you get a chance? Thanks 🙏

@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from 14ed9e0 to e3a3312 Compare August 3, 2026 13:34
@JerryLiu369 JerryLiu369 changed the title fix: skip content-fingerprint dedup for slash commands in weixin adapter fix(weixin): exempt slash commands from content-fingerprint dedup Aug 3, 2026
@JerryLiu369

Copy link
Copy Markdown
Author

Updated to a simpler approach: just exempt slash commands from content-fingerprint dedup entirely (if text and not text.startswith("/")). No command-specific TTL, no separate dedup cache, no injected clock.

The 3-second sliding window from the previous commit was over-engineered — message-ID dedup already handles iLink retries, and the only practical scenario it blocked was the exact one we want to permit (intentional retries of the same command).

This is now a +1/-1 production change with targeted regression tests. The approach matches what #29787, #29855, #35434, and earlier iterations of this PR all targeted — those were closed for unrelated bundled changes, not for the fix strategy itself.

cc @teknium1

@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from 0ee57f4 to 87a7b9a Compare August 20, 2026 04:28
@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from 87a7b9a to f03e54c Compare August 20, 2026 04:29
@JerryLiu369

Copy link
Copy Markdown
Author

Rebased onto latest main (b4f978d983). The current head (f03e54c486) uses the simpler scoped fix: slash commands bypass only the content-fingerprint dedup, while message-ID dedup remains unchanged. The earlier comments about the removed time-bucket implementation no longer apply.

Validation: tests/gateway/test_weixin.py::TestWeixinContentDedup — 3 passed; py_compile and git diff --check passed. The full file cannot run async tests in this environment because pytest-asyncio is unavailable. Could you take another look?

@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from f03e54c to c1af60d Compare August 27, 2026 05:33
JerryLiu369 added a commit to JerryLiu369/hermes-agent that referenced this pull request Aug 27, 2026
Simpler approach: if text starts with "/", skip the content-fingerprint
dedup entirely.  Message-ID dedup (300s TTL) still handles iLink network
retries, so the 3-second sliding window for commands is unnecessary.

This is the approach that NousResearch#29787, NousResearch#29855, NousResearch#35434, and NousResearch#36707 all
targeted — the earlier PRs were closed for bundled unrelated changes,
not for the fix strategy itself.

Tests:
- test_slash_commands_bypass_content_dedup: two /approve with different
  message IDs → both reach the handler
- test_slash_command_msg_id_dedup_still_applies: same message_id →
  deduped by message_id cache (not affected by this change)
- Existing test_duplicate_content_with_different_message_ids_is_dropped
  still passes (regular text dedup unchanged)
@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from c1af60d to bfa2165 Compare August 27, 2026 05:35
Simpler approach: if text starts with "/", skip the content-fingerprint
dedup entirely.  Message-ID dedup (300s TTL) still handles iLink network
retries, so the 3-second sliding window for commands is unnecessary.

This is the approach that NousResearch#29787, NousResearch#29855, NousResearch#35434, and NousResearch#36707 all
targeted — the earlier PRs were closed for bundled unrelated changes,
not for the fix strategy itself.

Tests:
- test_slash_commands_bypass_content_dedup: two /approve with different
  message IDs → both reach the handler
- test_slash_command_msg_id_dedup_still_applies: same message_id →
  deduped by message_id cache (not affected by this change)
- Existing test_duplicate_content_with_different_message_ids_is_dropped
  still passes (regular text dedup unchanged)
@JerryLiu369
JerryLiu369 force-pushed the fix/weixin-skip-content-dedup-for-slash-commands branch from bfa2165 to 1b28051 Compare September 12, 2026 06:11
@JerryLiu369

Copy link
Copy Markdown
Author

Closing for now. Thanks!

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

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have platform/wecom WeCom / WeChat Work adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants