feat(wa-gateway): compute wasMentioned from group_trigger_patterns when mentionedJids is empty#5230
Conversation
houko
left a comment
There was a problem hiding this comment.
Blockers
-
Caption extraction shape mismatch —
packages/whatsapp-gateway/index.js:1871-1876readsinnerMsg.documentMessage?.caption, but the existing text path atindex.js:1696correctly usesinnerMsg.documentWithCaptionMessage?.message?.documentMessage?.caption. A document-with-caption that mentions the agent by name will fail the fallback even though the daemon's check on the same message succeeds. Fix: reuse the already-computedtextvariable from line 1692 instead of re-extracting — single source of truth, no drift. -
(?i)translation is leading-only —compileGroupTriggerRegex(index.js:417-428) strips a leading(?i)only. Rust'sregexcrate accepts inline flags mid-pattern (foo(?i)bar,(?i)foo(?-i)bar). Such a pattern is daemon-valid, gateway-silent (no warn, just compiles a literal(?i)-containing regex that never matches). Either translate all inline flag forms, or warn when any(?flag group survives.
Non-blocking
- No telemetry log on fallback hit. PR body says the fix makes logs honest, but
index.js:1877-1880flipswasMentioned=truesilently. Add aconsole.log('[gateway] wasMentioned via group_trigger_patterns fallback: …')so operators can see how often Baileys'mentionedJidarray under-reports. - ReDoS surface. Patterns are user-supplied via
config.tomland compiled once at boot (good), but JSRegExp.testhas no timeout. A self-DoS via a pathological pattern only hurts the operator's own deployment, so low-severity, but worth documenting in[channels.whatsapp].group_trigger_patternsconfig docs. groupParticipantsordering: the fallback runs before thegroupParticipantsfetch (index.js:2151), but the daemon's OB-04 addressee guard uses participants to suppress mid-sentence matches. The gateway log will now claimwasMentioned=truefor "Caterina, chiedi al Signore…" while daemon will (correctly) abstain. PR scope says daemon is source of truth, so this is acceptable — but it inverts the original "logs lying" complaint in 1-of-N cases. Document this in the PR body.- Pattern array dedup with daemon-injected aliases:
channel_bridge.rs:2018auto-injects\bagent_name\bintogroup_trigger_patterns. The gateway reads the raw on-disk TOML and won't see those daemon-injected aliases — fallback will under-reportwasMentionedfor plain mentions of the agent name that haven't been manually added to the pattern list. Mention in PR body or fix in a follow-up.
Missing test coverage
packages/whatsapp-gateway/index.test.js has 1542 lines of tests and 0 for this change. Minimum:
compileGroupTriggerRegex('(?i)\\bbot\\b')returns case-insensitive matcher.compileGroupTriggerRegex('(')returnsnull+ warns (don't crash boot).- Fallback flips
wasMentionedtrue for plain-text mention whenmentionedJidsempty. - Fallback does NOT flip when patterns array is empty (current default).
- Empty / non-string entries handled.
Questions
- Why re-extract text in the fallback instead of reusing
text(line 1692)? Was the documentWithCaptionMessage shape difference intentional? - Should we mirror the daemon's
channel_bridge.rs:2018agent-name-alias auto-injection here so gateway and daemon agree without manual config? - Worth tagging the forwarded payload with
was_mentioned_source: "structured" | "pattern_fallback"so the daemon (and operators) can distinguish?
…rn, telemetry, tests Addresses CHANGES_REQUESTED on librefang#5230. Changes: Blockers - Caption shape mismatch: the fallback now reuses the `text` variable computed at index.js:1724, instead of re-extracting via a divergent expression that missed `documentWithCaptionMessage.message.documentMessage.caption`. Single source of truth, no drift with the daemon-side check. - Mid-pattern inline flag groups: `compileGroupTriggerRegex` now warns and skips Rust-style `foo(?i)bar`, `(?i)foo(?-i)bar`, `(?i:foo)bar`. JS RegExp silently produces a literal `(?…)` matcher for these and the pattern then never fires — surface it loudly at boot rather than let a TOML typo go quiet in production. Non-blocking - Telemetry: emit a structured `was_mentioned_pattern_fallback` log line every time the fallback flips wasMentioned true. Lets operators measure how often Baileys' structured `mentionedJid` array under-reports relative to plain-text mentions. - Provenance tag: forwarded payload now carries `was_mentioned_source: "structured" | "pattern_fallback"` when wasMentioned is true. Daemon and downstream observers can distinguish the two paths. Threaded through the streaming and non-streaming forward functions plus all SSE-fallback / retry call sites. Tests - New `describe('compileGroupTriggerRegex', …)` suite (6 cases): leading `(?i)` translation, invalid-pattern skip-with-warn, empty/non-string handling, mid-pattern inline-flag-group refusal with warn, default-case-sensitivity for non-flagged patterns, fallback decision simulation across the four input shapes (match / empty text / empty patterns / no match). - All new tests pass. The 2 pre-existing test failures (`forward_dispatch log` / `owner_notify channel`) are unrelated ECONNRESET flakes on the in-process mock HTTP server — confirmed identical fail count with the change stashed away.
|
@houko — addressed in
Question replies
Ready for re-review. |
|
Re-reviewed after
Two things now block merge that the author needs to resolve:
Code-wise the review feedback is resolved; please rebase onto current |
houko
left a comment
There was a problem hiding this comment.
Needs rebase (conflicting) — the branch is 133 commits behind main and packages/whatsapp-gateway/index.js conflicts with the root-level api_key (kernel bearer token) feature that landed after this branched. The collisions are purely additive textual adjacency, not semantic — both sides add new lines in the same three regions, so the resolution is mechanical:
readWhatsAppConfigdefaults object (~index.js:369): keep bothapi_key: ''andgroup_trigger_patterns: [].- config-parse block (~
index.js:385): keep both theapi_keyparse and thegroup_trigger_patternsparse. - post-
CONFIG_PATHboot section (~index.js:405-418): keep both theapi_keycomment/const and thecompileGroupTriggerRegex+GROUP_TRIGGER_REGEXESblock.
Once rebased onto green main, please confirm node --check + node --test packages/whatsapp-gateway/index.test.js still pass.
CI: the only red check (Workspace coverage (llvm-cov)) fails on a Rust opentelemetry_otlp trait-bound build error in librefang-telemetry — unrelated to this JS-only PR and a stale-base artifact. The rebase clears it; no action needed on the gateway code for that.
The logic itself is solid and the review feedback from the prior round was addressed well:
- Semantics match the daemon.
crates/librefang-channels/src/bridge.rs:1949usesRegexSet::new+is_match(unanchored substring), which lines up with JSRegExp.prototype.teston a non-global regex. The(?i)→i-flag translation incompileGroupTriggerRegex(index.js:417+) is correct: Rust honours inline(?i), JS does not, and stripping the leading prefix keeps a single config.toml working in both runtimes. - No over/under-trigger. The fallback at
index.js:1893is correctly gated onisGroup && !wasMentioned && text && GROUP_TRIGGER_REGEXES.length > 0, so it never fires when the structured mention already matched, whentextis empty, or when no patterns are configured.wasMentionedSourceprovenance (structuredvspattern_fallback) is a nice honest-signal addition. - No end-user regex injection. Patterns come from operator
config.toml(bridge.rs:295notes they're already-escaped), not from inbound WhatsApp text, so the JS-backtracking ReDoS surface is operator-self-inflicted only — acceptable, matching the Rust trust model. - Mid-pattern inline-flag guard (
(?i:…),foo(?i)bar) is a genuinely good catch — JS would silently treat those as literals and never match; warning + skip is the right call. - Tests (
index.test.js) cover all four required fallback cases plus the(?i)flag, invalid-pattern skip, non-string skip, and mid-pattern refusal.compileGroupTriggerRegexis exported (index.js:3888) for the unit test.
No determinism concern: these patterns feed a gateway boolean, not an LLM prompt.
Requesting changes only for the rebase. The diff is good to merge once it's no longer conflicting.
…en WA mentionedJids is empty
Pre-fix the gateway's `wasMentioned` flag was set ONLY when WhatsApp's
structured `contextInfo.mentionedJid` array contained the agent's
number — i.e. when the user had explicitly tapped `@` in the compose UI
to insert a mention. Plain-text references ("Ambrogio, ciao") never set
the flag, so:
- The forwarded payload sent `was_mentioned: false` to the daemon even
when the agent name was literally typed in the message text.
- Gateway-side logs showed `was_mentioned:false` for messages that the
daemon then matched via `[channels.whatsapp].group_trigger_patterns`
and replied to anyway — a confusing log signal.
The Rust daemon already computes the pattern match independently via
`compile_group_trigger_patterns` in `librefang-channels::bridge`, so
this was a signal-accuracy gap, not a behavioural break.
- `readWhatsAppConfig` parses `[channels.whatsapp].group_trigger_patterns`
(array of strings; defaults to `[]`).
- Module-level `compileGroupTriggerRegex(pattern)` converts each pattern
to a JS `RegExp`. The Rust daemon uses Rust `regex` which honours the
`(?i)` inline flag; JavaScript regexes do not — `new RegExp("(?i)foo")`
matches the literal three-character `(?i)` prefix instead of enabling
case-insensitive matching. Strip a leading `(?i)` and translate it to
the JS `i` flag so the same `config.toml` is honoured verbatim in
both runtimes. Invalid patterns are skipped with a `console.warn`.
- After the structured-mention check, fall back to matching the message
text (`conversation` / `extendedTextMessage.text` / image/video/document
caption) against the compiled regex set. Set `wasMentioned = true`
on first match.
Gateway-side signal fix only. Forwarding behaviour and gating are
unchanged. The daemon stays the source of truth for `mention_only`
enforcement; the gateway just stops reporting `was_mentioned:false`
when the message literally mentioned the agent.
`node --check packages/whatsapp-gateway/index.js` clean.
…rn, telemetry, tests Addresses CHANGES_REQUESTED on librefang#5230. Changes: Blockers - Caption shape mismatch: the fallback now reuses the `text` variable computed at index.js:1724, instead of re-extracting via a divergent expression that missed `documentWithCaptionMessage.message.documentMessage.caption`. Single source of truth, no drift with the daemon-side check. - Mid-pattern inline flag groups: `compileGroupTriggerRegex` now warns and skips Rust-style `foo(?i)bar`, `(?i)foo(?-i)bar`, `(?i:foo)bar`. JS RegExp silently produces a literal `(?…)` matcher for these and the pattern then never fires — surface it loudly at boot rather than let a TOML typo go quiet in production. Non-blocking - Telemetry: emit a structured `was_mentioned_pattern_fallback` log line every time the fallback flips wasMentioned true. Lets operators measure how often Baileys' structured `mentionedJid` array under-reports relative to plain-text mentions. - Provenance tag: forwarded payload now carries `was_mentioned_source: "structured" | "pattern_fallback"` when wasMentioned is true. Daemon and downstream observers can distinguish the two paths. Threaded through the streaming and non-streaming forward functions plus all SSE-fallback / retry call sites. Tests - New `describe('compileGroupTriggerRegex', …)` suite (6 cases): leading `(?i)` translation, invalid-pattern skip-with-warn, empty/non-string handling, mid-pattern inline-flag-group refusal with warn, default-case-sensitivity for non-flagged patterns, fallback decision simulation across the four input shapes (match / empty text / empty patterns / no match). - All new tests pass. The 2 pre-existing test failures (`forward_dispatch log` / `owner_notify channel`) are unrelated ECONNRESET flakes on the in-process mock HTTP server — confirmed identical fail count with the change stashed away.
c27199d to
48315bc
Compare
|
Rebased onto current
|
houko
left a comment
There was a problem hiding this comment.
Re-reviewed after the latest push — fully addressed, approving. Verified the fallback reuses the single text source (index.js:1801, used at :1954) so document-caption shape no longer drifts from the daemon check; compileGroupTriggerRegex correctly maps a leading (?i) to the JS i flag and rejects mid-pattern Rust inline-flag groups; semantics match the daemon's unanchored RegexSet match (bridge.rs:2141); and the rebase kept both api_key and the new parsing.
Two nits, neither blocking: the comment at :1949 references "line 1724" but text is at :1801 (stale), and feat: vs fix: in the title is borderline given the new config + payload field.
houko
left a comment
There was a problem hiding this comment.
Re-reviewed at the current head: JS-only wa-gateway change with a test suite (index.test.js), rebased clean, CI green. The fallback wasMentioned-from-group_trigger_patterns logic is sound and gated behind the empty-mentionedJids branch. LGTM.
Summary
The gateway's
wasMentionedflag was set only on WhatsApp's structured@-mentions. Plain-text references to the agent name ("Ambrogio,conferma...") came through with
was_mentioned: false, even though theRust daemon's
compile_group_trigger_patternsthen matched the textand replied. Result: gateway logs and the forwarded
was_mentionedfield lied to the daemon and to operators reading the logs.
Changes
[channels.whatsapp].group_trigger_patternsinreadWhatsAppConfig(defaults to[]).compileGroupTriggerRegex(pattern)converts each pattern to a JSRegExp. The Rust daemon honours(?i)inline; JavaScript does not(
new RegExp("(?i)foo")literally matches(?i)foo). Strip a leading(?i)and translate it to the JSiflag so the same config.tomlworks in both runtimes. Invalid patterns are skipped with a
warn.text (conversation / extendedTextMessage / image-video-document
caption) against the compiled regex set.
Scope
Gateway-side signal fix only. Forwarding behaviour and daemon-side
gating are unchanged. The daemon stays the source of truth for
mention_onlyenforcement.Verification
node --check packages/whatsapp-gateway/index.jsclean. Live-testedon a deployment where "Ambrogio, conferma in 5 parole..." in a group
chat produced
was_mentioned:falseat the gateway pre-fix; post-fixthe gateway log correctly reflects the daemon's pattern match.