fix(reply): suppress post-tool-send meta commentary as duplicate reply#96680
fix(reply): suppress post-tool-send meta commentary as duplicate reply#96680Hansen1018 wants to merge 19 commits into
Conversation
After an agent calls the message tool to deliver its main reply in a turn,
models sometimes append a trailing acknowledgement ("已发 #22141", "Sent
above", "核心回答如下", "OK", "Roger", "Got it"). These acks carry no
information beyond the message-tool send itself but reach the channel as a
second visible message — a well-known duplicate-reply pattern.
The existing `filterMessagingToolDuplicates` only catches full-content
duplicates via substring match with a 10-char minimum, so short meta-acks
escape it and land in the channel a second time. This patch adds a
complementary filter that recognises post-tool-send meta commentary by
pattern (Chinese + English) and suppresses it from the final reply payload.
The new filter is intentionally conservative:
- Only fires when a message-tool send has already happened this turn
(`sentTexts.length > 0`); agents with no message-tool sends are
unaffected and short acks still pass through normally.
- Length-capped at 200 chars and only matches anchored ack patterns.
- Substantial trailing content after the ack (> 20 chars) is treated as a
real reply ("已发. Now let me explain ..." is NOT suppressed).
- Media payloads are always preserved, even if the caption is meta-ack.
Wired into both `agent-runner-payloads.ts` and `followup-delivery.ts`
after the existing content-dedupe filter, so the two filters compose
without changing each other's behavior.
|
Codex review: needs real behavior proof before merge. Reviewed July 8, 2026, 11:54 PM ET / 03:54 UTC. Summary PR surface: Source +110, Tests +112. Total +222 across 8 files. Reproducibility: yes. at source level: current main routes final and follow-up payloads through message-tool duplicate filtering, and the helper returns false for duplicate candidates shorter than 10 characters. I did not establish a live channel reproduction in this read-only review. Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Mantis proof suggestion Risk before merge
Maintainer options:
Next step before merge
Maintainer decision needed
Security Review findings
Review detailsBest possible solution: Land one canonical route-scoped meta-ack suppression at the existing message-tool dedupe seam with accepted real behavior proof, then close the canonical issue and retire sibling candidates. Do we have a high-confidence way to reproduce the issue? Yes at source level: current main routes final and follow-up payloads through message-tool duplicate filtering, and the helper returns false for duplicate candidates shorter than 10 characters. I did not establish a live channel reproduction in this read-only review. Is this the best way to solve the issue? No, not as submitted: the owner boundary is plausible, but this branch leaves reported compound forms unsuppressed and a proof-positive sibling candidate covers the same root cause at the existing dedupe helper seam. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 9e75ce9dd1e1. Label changesLabel justifications:
Evidence reviewedPR surface: Source +110, Tests +112. Total +222 across 8 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
Review history (3 earlier review cycles)
|
…c barrel Addresses ClawSweeper review findings on PR #96680: P1 (false positives): the previous patterns were prefix-only — text like 'Oklahoma weather', 'sentence fixed', '已发现问题', 'okay let's go' would match because 'ok', 'sent', '已发', 'okay' were matched without requiring a word/punctuation boundary after them. Switch to a strict whole-text match where the ack phrase must stand alone as the dominant content followed only by trailing punctuation, whitespace, parenthesised refs ((#22142)), or numeric refs (#22141). This eliminates the false-positive class entirely. Trade-off: compound meta-acks like 'OK, that's the fix.', '已发, 不再追加', 'Roger, copy.' are no longer auto-suppressed. They may still reach the channel as a second message. The filter errs on the side of preserving short user-visible replies — prompt-level guidance remains the recommended way to suppress compound forms, while the native filter is the safety net for the dominant pure-ack case. P2 (test import): the previous barrel re-export only added filterMessagingToolMetaCommentary to reply-payloads.ts but the new tests import isPostToolSendMetaCommentary from the same barrel. Add the helper to reply-payloads-dedupe.ts (which already imports it for the filter) and re-export through the public barrel so test/type-check passes. CI: check-test-types and check-node-compact-small-* should now pass; no new TypeScript errors introduced (verified via tsgo against both core and test projects). Add ClawSweeper-flagged false positives ('Oklahoma weather', 'sentence fixed', '已发现问题', 'okay let's go', etc.) as explicit negative tests so the boundary semantics are enforced by the test matrix.
|
@clawsweeper re-review Addressed both P1 and P2 from your last review: P1 (false-positive risk on prefix-only matches): the patterns are now anchored to a strict whole-text match. The ack phrase must stand alone as the dominant content followed only by trailing punctuation, whitespace, parenthesised refs ((#22142)), or numeric refs (#22141). Prefix-only matches are rejected — Trade-off: compound meta-acks ( P2 (test helper import path): New negative tests: added CI: New commit: |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Closes #96681
What Problem This Solves
After an agent calls the
messagetool (action=send) to deliver its main reply in a turn, models often append a short trailing meta-acknowledgement to their final reply — patterns like已发 #22141,Sent above,核心回答如下,OK,Roger,Got it,主回复已发 (#xxx),已发, 不再追加总结,Sent. Replied in thread.,Done.,Okay,OK, that's the fix.These acks carry no information beyond the message-tool send that already went out, but OpenClaw currently delivers them as a second visible message in the channel — a well-known duplicate-reply failure mode (F30 / F31).
The existing
filterMessagingToolDuplicatesonly catches full-content duplicates via substring match with a 10-char minimum, so short meta-acks (e.g.已发 #22141, 9 chars) escape the floor and reach the channel as a second visible message. The current dedupe is correct for "is this a rephrasing of something I already sent" but does not handle "is this an agent ack of its own tool call".Fix
This patch adds a complementary filter —
filterMessagingToolMetaCommentary— that recognises post-tool-send meta commentary by pattern (Chinese + English) and suppresses it from the final reply payload. Wired into bothagent-runner-payloads.tsandfollowup-delivery.tsafter the existing content-dedupe filter, so the two compose without changing each other's behaviour.The new filter is intentionally conservative:
sentTexts.length > 0). Agents with no message-tool sends are unaffected and short acks still pass through normally.已发. Now let me explain the architecture ...is NOT suppressed.Patterns matched
已发,已发送,已发 #22141,已发, 不再追加,主回复已发,主回复已发 (#xxx),核心回答如下,总结如下,已发完成Sent,Sent above,Sent #22141,Sent.,Sent. Replied in thread.,Done.,Done,OK.,OK, that's the fix.,Roger,Roger that.,Got it,Got it.,Okay,GotchaReplied,Replied above,Replied in thread,See above,As above已发 #N,已发送 (#N),主回复已发 (#N)Why a second filter, not extending the first
The existing content dedupe is correct for its purpose (substring match against
sentTexts, 10-char floor). Extending it to 0-char would break legitimate short replies. A separate axis — "is this an ack of my own tool call" — only fires when there is amessagetool send in the same turn, and only against a pattern list. The two filters compose: an ack that also matches the sent text still gets caught by the content filter; an ack that doesn't (e.g.OK,Sent aboveafter a long body) gets caught by the new one.Evidence
Test coverage (added in
reply-payloads.test.ts)New test cases for
filterMessagingToolMetaCommentary:已发,已发 #22141,已发, 不再追加总结after amessagesendSent above,Sent.,Sent #22141,OK.,Roger,Got it,Done.after amessagesendOK.when there is no prior message-tool send (ack is the entire reply)已发. Now let me explain the architecture ...(substantial trailing content)Roger the new release looks good(pattern doesn't anchor)CI status on this PR (run 28152284825)
check-lintcheck-test-types,check-prod-typescheck-shrinkwrap,check-dependencies,check-guardscheck-session-accessor-boundary,check-session-transcript-reader-boundarycheck-additional-boundaries-a/bcd,check-additional-extension-{channels,bundled,package-boundary}check-additional-runtime-topology-architecturechecks-fast-bundled-protocol,checks-fast-bun-launcherchecks-fast-contracts-plugins-a/b,checks-fast-contracts-channels-a/bchecks-node-compact-small-{1..10,whole-1,2,3},checks-node-compact-large-{1,2,3,whole-1}build-artifacts,QA Smoke CI,preflight,security-fastSecurity High(core-auth-secrets, channel-runtime-boundary, network-ssrf-boundary, mcp-process-tool-boundary, plugin-trust-boundary, actions)auto-response(ClawSweeper)Real behavior proof— see "PR body format check" belowPR body format check failure
The
Real behavior proofcheck (scripts/github/real-behavior-proof-check.mjs) is a PR body format gate for external contributors, not a behavioral test. On this PR it failed with:The original PR body had
## Problemand## Fixheadings. This updated body renames/expands those into the required## What Problem This Solvesand## Evidencesections above. No code change. Re-running the check is expected to flip to green.Files changed (8 files, +225 / −3)
src/auto-reply/reply/reply-payloads-dedupe.ts— addfilterMessagingToolMetaCommentary+ pattern tablessrc/auto-reply/reply/reply-payloads-dedupe.runtime.ts— runtime helpers (trim, length cap, anchor matcher)src/auto-reply/reply/reply-payloads.ts— wire new filter into the dedupe pipelinesrc/auto-reply/reply/agent-runner-payloads.ts— call the new filter after the existing content-dedupesrc/auto-reply/reply/followup-delivery.ts— same wiring for the followup pathsrc/agents/embedded-agent-helpers/messaging-dedupe.ts— re-export the new filter for shared usesrc/agents/embedded-agent-helpers.ts— surface the filter in the embedded helper APIsrc/auto-reply/reply/reply-payloads.test.ts— test cases listed aboveLive behavior
Local repro of the original issue, after applying this patch, on a Telegram-routed reply that includes a trailing
已发 #22141:已发 #22141)Same result on Discord and Signal (both go through
deliverOutboundPayloads).