Skip to content

feat(wa-gateway): compute wasMentioned from group_trigger_patterns when mentionedJids is empty#5230

Merged
houko merged 2 commits into
librefang:mainfrom
f-liva:fix/wa-gateway-mention-pattern-fallback
May 23, 2026
Merged

feat(wa-gateway): compute wasMentioned from group_trigger_patterns when mentionedJids is empty#5230
houko merged 2 commits into
librefang:mainfrom
f-liva:fix/wa-gateway-mention-pattern-fallback

Conversation

@f-liva

@f-liva f-liva commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

The gateway's wasMentioned flag 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 the
Rust daemon's compile_group_trigger_patterns then matched the text
and replied. Result: gateway logs and the forwarded was_mentioned
field lied to the daemon and to operators reading the logs.

Changes

  • Parse [channels.whatsapp].group_trigger_patterns in
    readWhatsAppConfig (defaults to []).
  • compileGroupTriggerRegex(pattern) converts each pattern to a JS
    RegExp. 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 JS i flag so the same config.toml
    works in both runtimes. Invalid patterns are skipped with a warn.
  • After the structured-mention scan, fall back to matching the message
    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_only enforcement.

Verification

node --check packages/whatsapp-gateway/index.js clean. Live-tested
on a deployment where "Ambrogio, conferma in 5 parole..." in a group
chat produced was_mentioned:false at the gateway pre-fix; post-fix
the gateway log correctly reflects the daemon's pattern match.

@github-actions github-actions Bot added area/sdk JavaScript and Python SDKs no-rust-required This task does not require Rust knowledge size/M 50-249 lines changed labels May 18, 2026

@houko houko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blockers

  1. Caption extraction shape mismatchpackages/whatsapp-gateway/index.js:1871-1876 reads innerMsg.documentMessage?.caption, but the existing text path at index.js:1696 correctly uses innerMsg.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-computed text variable from line 1692 instead of re-extracting — single source of truth, no drift.

  2. (?i) translation is leading-onlycompileGroupTriggerRegex (index.js:417-428) strips a leading (?i) only. Rust's regex crate 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-1880 flips wasMentioned=true silently. Add a console.log('[gateway] wasMentioned via group_trigger_patterns fallback: …') so operators can see how often Baileys' mentionedJid array under-reports.
  • ReDoS surface. Patterns are user-supplied via config.toml and compiled once at boot (good), but JS RegExp.test has 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_patterns config docs.
  • groupParticipants ordering: the fallback runs before the groupParticipants fetch (index.js:2151), but the daemon's OB-04 addressee guard uses participants to suppress mid-sentence matches. The gateway log will now claim wasMentioned=true for "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:2018 auto-injects \bagent_name\b into group_trigger_patterns. The gateway reads the raw on-disk TOML and won't see those daemon-injected aliases — fallback will under-report wasMentioned for 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('(') returns null + warns (don't crash boot).
  • Fallback flips wasMentioned true for plain-text mention when mentionedJids empty.
  • Fallback does NOT flip when patterns array is empty (current default).
  • Empty / non-string entries handled.

Questions

  1. Why re-extract text in the fallback instead of reusing text (line 1692)? Was the documentWithCaptionMessage shape difference intentional?
  2. Should we mirror the daemon's channel_bridge.rs:2018 agent-name-alias auto-injection here so gateway and daemon agree without manual config?
  3. Worth tagging the forwarded payload with was_mentioned_source: "structured" | "pattern_fallback" so the daemon (and operators) can distinguish?

@github-actions github-actions Bot added the needs-changes Changes requested by reviewer label May 19, 2026
f-liva pushed a commit to f-liva/librefang that referenced this pull request May 19, 2026
…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.
@f-liva

f-liva commented May 19, 2026

Copy link
Copy Markdown
Contributor Author

@houko — addressed in 9a5f09ec.

# Item Status Evidence
B1 Caption shape mismatch (documentMessage vs documentWithCaptionMessage.message.documentMessage) Fixed Fallback now reuses the text variable from index.js:1724 — single source of truth, no shape drift. Re-extraction removed.
B2 (?i) translation is leading-only Fixed compileGroupTriggerRegex now detects any surviving (?<flags>…) / (?<flags>:…) group via /\(\?[ismUuxa-]+[):]/ and bails out with a warn. Three forms covered by tests: foo(?i)bar, (?i)foo(?-i)bar, (?i:foo)bar.
N1 No telemetry log on fallback hit Fixed Added single-line was_mentioned_pattern_fallback JSON log on every flip (chat_jid, push_name, text_length).
N2 ReDoS surface Documented Self-DoS only; noted as a config hygiene concern. Will fold into [channels.whatsapp].group_trigger_patterns config docs separately so this PR stays scoped.
N3 groupParticipants ordering vs OB-04 Acknowledged Daemon remains source of truth; the rare "gateway says mentioned, daemon abstains via OB-04" case is now distinguishable in logs thanks to the new was_mentioned_source tag (see Q3). PR body updated.
N4 Daemon-injected alias awareness Acknowledged Looked at bridge.rs:~2960 — aliases are computed dynamically from channel_default_name and group_trigger_patterns, not injected into the patterns array on disk. The gateway sees the same on-disk patterns the daemon does; the daemon's additional agent_name alias for reply-intent classification is a separate ephemeral path. No drift to fix here; mirror-injection would duplicate state. PR body updated to clarify.
T Missing test coverage Fixed New describe('compileGroupTriggerRegex', …) block, 6 cases: leading (?i) translation, syntactically-invalid skip-with-warn, empty/non-string handling, mid-pattern flag-group refusal, default case-sensitivity, fallback decision simulation over the four input shapes. npm test ⇒ all 6 pass; the 2 pre-existing red tests (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.

Question replies

  • Q1 Was the divergent extraction intentional? No — drafting error. Reusing text is the right answer; done.
  • Q2 Mirror the daemon's alias auto-injection here? See N4 — the daemon doesn't inject into group_trigger_patterns; it computes a separate alias at classify time. No mirror needed.
  • Q3 Tag the payload with was_mentioned_source? Yes — done. Forwarded payload now carries was_mentioned_source: "structured" | "pattern_fallback" when wasMentioned is true (omitted when false — null carries no signal). Threaded through both forwardToLibreFang and forwardToLibreFangStreaming, plus the SSE-fallback and 404-retry call sites. Also added to the forward_dispatch log line on both paths.

Ready for re-review.

@github-actions github-actions Bot added has-conflicts PR has merge conflicts that need resolution and removed has-conflicts PR has merge conflicts that need resolution labels May 20, 2026
@houko

houko commented May 22, 2026

Copy link
Copy Markdown
Contributor

Re-reviewed after 9a5f09ec (pushed 2026-05-19, after the CHANGES_REQUESTED review on 12688a58). The substantive review blockers look addressed:

  1. Caption shape mismatch — the fallback now reuses the already-computed text variable instead of re-extracting (packages/whatsapp-gateway/index.js:1889-1900), so documentWithCaptionMessage captions no longer drift from the daemon-side check.
  2. Inline flag groupscompileGroupTriggerRegex (index.js:421) now detects surviving Rust-style (?…) groups via /\(\?[ismUuxa-]+[):]/ and warns+skips foo(?i)bar, (?i)foo(?-i)bar, (?i:foo)bar rather than compiling a literal-never-matches regex.
  3. Telemetry + provenance — fallback hits emit a structured was_mentioned_pattern_fallback line (index.js:1902) and the forwarded payload carries was_mentioned_source: "structured" | "pattern_fallback", threaded through both forward functions and all SSE-fallback / retry call sites.
  4. Tests — new describe('compileGroupTriggerRegex', …) in index.test.js covers leading (?i) translation, invalid-pattern skip-with-warn, empty/non-string handling, mid-pattern inline-flag refusal, default case-sensitivity, and the four-way fallback decision. The helper is exported (index.js:3887).

Two things now block merge that the author needs to resolve:

Code-wise the review feedback is resolved; please rebase onto current main and get CI green. @houko can then re-review/dismiss the stale CHANGES_REQUESTED.

@houko houko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  • readWhatsAppConfig defaults object (~index.js:369): keep both api_key: '' and group_trigger_patterns: [].
  • config-parse block (~index.js:385): keep both the api_key parse and the group_trigger_patterns parse.
  • post-CONFIG_PATH boot section (~index.js:405-418): keep both the api_key comment/const and the compileGroupTriggerRegex + GROUP_TRIGGER_REGEXES block.

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:1949 uses RegexSet::new + is_match (unanchored substring), which lines up with JS RegExp.prototype.test on a non-global regex. The (?i)i-flag translation in compileGroupTriggerRegex (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:1893 is correctly gated on isGroup && !wasMentioned && text && GROUP_TRIGGER_REGEXES.length > 0, so it never fires when the structured mention already matched, when text is empty, or when no patterns are configured. wasMentionedSource provenance (structured vs pattern_fallback) is a nice honest-signal addition.
  • No end-user regex injection. Patterns come from operator config.toml (bridge.rs:295 notes 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. compileGroupTriggerRegex is 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.

Federico Liva added 2 commits May 22, 2026 20:20
…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.
@f-liva
f-liva force-pushed the fix/wa-gateway-mention-pattern-fallback branch from c27199d to 48315bc Compare May 22, 2026 18:21
@f-liva

f-liva commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current origin/main. All three conflicts in packages/whatsapp-gateway/index.js were the textually-adjacent additive interleaves you described:

  1. readWhatsAppConfig defaults — kept both api_key: '' and group_trigger_patterns: [].
  2. config-parse block — kept both the api_key parse and the group_trigger_patterns parse.
  3. Post-CONFIG_PATH boot section — kept the api_key const + kernelAuthHeader helper followed by compileGroupTriggerRegex + GROUP_TRIGGER_REGEXES.

node --check packages/whatsapp-gateway/index.js passes. Couldn't run node --test locally (toml dep not installed in this worktree); will rely on CI.

@github-actions github-actions Bot removed the has-conflicts PR has merge conflicts that need resolution label May 22, 2026

@houko houko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@github-actions github-actions Bot added ready-for-review PR is ready for maintainer review and removed needs-changes Changes requested by reviewer labels May 22, 2026

@houko houko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@houko
houko merged commit ed81e69 into librefang:main May 23, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sdk JavaScript and Python SDKs no-rust-required This task does not require Rust knowledge ready-for-review PR is ready for maintainer review size/M 50-249 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants