@@ -5,6 +5,38 @@ import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/st
55
66const MIN_DUPLICATE_TEXT_LENGTH = 10 ;
77const MIN_REVERSE_SUBSTRING_DUPLICATE_RATIO = 0.5 ;
8+ // Maximum length for a "post-tool-send meta commentary" candidate. Real
9+ // replies are longer than this; meta-acks ("已发 #22141", "Sent above")
10+ // are short trailing acknowledgements after a message-tool send.
11+ const MAX_META_COMMENTARY_LENGTH = 200 ;
12+ // Sentence-boundary delimiters for compound ack splitting.
13+ const SENTENCE_BOUNDARY_RE = / [ . ; , 。 , ; ! ? ! \n ] \s * / g;
14+
15+ /**
16+ * Patterns that match a post-tool-send meta-acknowledgement phrase.
17+ *
18+ * Each pattern is anchored (`^...$`) and matches the ack phrase followed
19+ * **only** by trailing punctuation, whitespace, parenthesised message refs
20+ * (`(#22142)`), or numeric refs (`#22141`). The ack phrase itself must
21+ * stand alone as the dominant content — prefix-only matches are rejected
22+ * so that real replies like `Oklahoma weather`, `sentence fixed`,
23+ * `已发现问题`, `okay let's go` are NOT suppressed.
24+ *
25+ * Compound meta-acks (`Sent. Replied in thread.`, `已发, 不再追加总结`,
26+ * `OK. Done.`) are detected by splitting on sentence boundaries and
27+ * checking that every segment independently matches a pattern from this
28+ * list. See `isCompoundMetaCommentary`.
29+ */
30+ const META_COMMENTARY_PATTERNS : readonly RegExp [ ] = [
31+ // Chinese standalone acks: "已发", "已发送", "主回复已发", "好了", "收到"
32+ / ^ (?: 已 发 (?: 送 | 完 毕 | 完 成 ) ? | 主 回 复 已 发 | 消 息 已 发 (?: 出 | 送 ) ? | 回 复 已 发 | 好 了 ? | 收 到 | 完 毕 | 完 成 | 了 解 | 知 道 了 | 明 白 ) [ \s . , 。 ! ; ; ? ? 、 ( ) # \d ] * $ / u,
33+ // Chinese mid-text acks: "核心回答如下", "总结如下", "不再追加"
34+ / ^ (?: 核 心 回 答 如 下 | 总 结 如 下 | 以 下 为 核 心 (?: 回 答 | 内 容 ) ? | 以 下 为 总 结 | 不 再 追 加 (?: 总 结 | 内 容 ) ? | 以 下 为 回 复 | 答 案 如 下 | 如 上 | 回 复 如 上 ) [ \s . , 。 ! ; ; ? ? 、 ( ) # \d ] * $ / u,
35+ // English standalone acks: "OK", "Sent", "Done", "Roger", "Got it", ...
36+ / ^ (?: s e n t (?: \s + (?: a b o v e | # \d + | \( [ ^ ) ] + \) ) ) ? | d o n e \. ? | r e p l i e d (?: \s + (?: a b o v e | i n \s + t h r e a d ) ) ? | s e e \s + a b o v e | a s \s + a b o v e | p o s t e d \. ? | a c k n o w l e d g e d \. ? | o k (?: a y ) ? | g o t \s + i t | g o t c h a | r o g e r | c o p (?: y | i e d ) (?: \s + t h a t ) ? | a c k (?: n o w l e d g e d ) ? | w i l l \s + d o | o n \s + i t | n o t e d | u n d e r s t o o d | t h a n k s | t h x ) [ \s . , ! ; ; ? ? ( ) # \d ] * $ / i,
37+ // English mid-text: "Replying above", "Answer below", "Response above"
38+ / ^ (?: r e p l y i n g \s + (?: a b o v e | b e l o w | i n \s + t h r e a d ) | a n s w e r \s + b e l o w | r e s p o n s e \s + a b o v e | s e n t \s + i n \s + t h r e a d ) [ \s . , ! ; ; ? ? ( ) # \d ] * $ / i,
39+ ] ;
840
941/**
1042 * Normalize text for duplicate comparison.
@@ -20,6 +52,60 @@ export function normalizeTextForComparison(text: string): string {
2052 . trim ( ) ;
2153}
2254
55+ /**
56+ * Detect compound meta-acks where every sentence-boundary-delimited
57+ * segment is itself a short meta-acknowledgement.
58+ *
59+ * Used as a second pass after the full-text anchored pattern match
60+ * so that multi-sentence acks like `Sent. Replied in thread.` and
61+ * `已发, 不再追加总结` are caught, while mixed-form texts like
62+ * `OK, that's the fix.` or `已发. Now let me explain...` are
63+ * preserved because at least one segment does not match any ack
64+ * pattern.
65+ */
66+ function isCompoundMetaCommentary ( normalized : string ) : boolean {
67+ const segments = normalized . split ( SENTENCE_BOUNDARY_RE ) . filter ( ( s ) => s . trim ( ) . length > 0 ) ;
68+ if ( segments . length < 2 ) return false ;
69+ // Every segment must independently match a meta-ack pattern.
70+ // A single non-matching segment means the text has real content.
71+ return segments . every ( ( segment ) =>
72+ META_COMMENTARY_PATTERNS . some ( ( pattern ) => pattern . test ( segment . trim ( ) ) ) ,
73+ ) ;
74+ }
75+
76+ /**
77+ * Detect short post-tool-send meta commentary.
78+ *
79+ * After an agent runs a message-tool send in the same turn, models
80+ * sometimes append a trailing acknowledgement like "已发 #22141",
81+ * "Sent above", "核心回答如下", "OK", "Roger" or "Got it". These
82+ * acks add no information beyond the message-tool send itself and
83+ * reach the channel as a second visible message — a well-known
84+ * duplicate-reply pattern.
85+ *
86+ * Detection is intentionally conservative:
87+ * - Normalized text must be ≤ MAX_META_COMMENTARY_LENGTH chars
88+ * - Full-text anchored pattern match is attempted first (standalone
89+ * acks like `已发`, `OK`, `Sent above`)
90+ * - If that fails, compound ack detection splits on sentence
91+ * boundaries and verifies every segment is itself ack-like
92+ * - Used only when a message-tool send has already happened this
93+ * turn (caller responsibility — see
94+ * `filterMessagingToolMetaCommentary`)
95+ */
96+ export function isPostToolSendMetaCommentary ( text : string ) : boolean {
97+ const normalized = normalizeTextForComparison ( text ) ;
98+ if ( ! normalized || normalized . length > MAX_META_COMMENTARY_LENGTH ) {
99+ return false ;
100+ }
101+ // Full-text anchored match first (standalone acks).
102+ if ( META_COMMENTARY_PATTERNS . some ( ( pattern ) => pattern . test ( normalized ) ) ) {
103+ return true ;
104+ }
105+ // Second pass: compound ack detection.
106+ return isCompoundMetaCommentary ( normalized ) ;
107+ }
108+
23109/** Compare already-normalized message text against prior sends. */
24110export function isMessagingToolDuplicateNormalized (
25111 normalized : string ,
0 commit comments