fix(gateway): canonicalize WhatsApp identity in session keys - #14904
Closed
keiravoss94 wants to merge 1 commit into
Closed
fix(gateway): canonicalize WhatsApp identity in session keys#14904keiravoss94 wants to merge 1 commit into
keiravoss94 wants to merge 1 commit into
Conversation
github-actions
Bot
force-pushed
the
feature/whatsapp-dm-canonical-session-key
branch
from
April 24, 2026 05:01
7c4771d to
7ee1795
Compare
keiravoss94
force-pushed
the
feature/whatsapp-dm-canonical-session-key
branch
3 times, most recently
from
April 24, 2026 10:28
d0f0a9d to
5459465
Compare
keiravoss94
force-pushed
the
feature/whatsapp-dm-canonical-session-key
branch
from
April 24, 2026 10:35
5459465 to
08352d0
Compare
Hermes' WhatsApp bridge routinely surfaces the same person under either a phone-format JID (60123456789@s.whatsapp.net) or a LID (…@lid), and may flip between the two for a single human within the same conversation. Before this change, build_session_key used the raw identifier verbatim, so the bridge reshuffling an alias form produced two distinct session keys for the same person — in two places: 1. DM chat_id — a user's DM sessions split in half, transcripts and per-sender state diverge. 2. Group participant_id (with group_sessions_per_user enabled) — a member's per-user session inside a group splits in half for the same reason. Add a canonicalizer that walks the bridge's lid-mapping-*.json files and picks the shortest/numeric-preferred alias as the stable identity. build_session_key now routes both the DM chat_id and the group participant_id through this helper when the platform is WhatsApp. All other platforms and chat types are untouched. Expose canonical_whatsapp_identifier and normalize_whatsapp_identifier as public helpers. Plugins that need per-sender behaviour (role-based routing, per-contact authorization, policy gating) need the same identity resolution Hermes uses internally; without a public helper, each plugin would have to re-implement the walker against the bridge's internal on-disk format. Keeping this alongside build_session_key makes it authoritative and one refactor away if the bridge ever changes shape. _expand_whatsapp_aliases stays private — it's an implementation detail of how the mapping files are walked, not a contract callers should depend on.
keiravoss94
force-pushed
the
feature/whatsapp-dm-canonical-session-key
branch
from
April 24, 2026 10:40
08352d0 to
f75a53a
Compare
Contributor
|
Your fix and regression suite were cherry-picked onto current main with your authorship preserved (commit 10deb1b on main). A small follow-up commit on top extracts the JID/LID helpers into a new Thanks for the careful writeup and the public-helper contract tests. |
github-actions Bot
pushed a commit
to pebble-tech/hermes-agent
that referenced
this pull request
Apr 27, 2026
This is the sole commit on ops-overlay. It carries:
- .github/workflows/sync-upstream.yml: daily rebase + rebuild of main
on top of upstream/main + ops-overlay + each feature branch.
- FORK.md: branch layout, sync model, recovery procedure.
Steady state (2026-04-26 onward): both feature branches we used to
carry have merged upstream (PRs NousResearch#13445 and NousResearch#14904 via NousResearch#15050 and
NousResearch#15191). FEATURE_BRANCHES is now empty; main collapses to
upstream/main + ops-overlay. The fork stays alive so customer VPSes
have a stable deploy target rebuilt on our schedule.
github-actions Bot
pushed a commit
to pebble-tech/hermes-agent
that referenced
this pull request
Apr 30, 2026
This is the sole commit on ops-overlay. It carries:
- .github/workflows/sync-upstream.yml: daily rebase + rebuild of main
on top of upstream/main + ops-overlay + each feature branch.
- FORK.md: branch layout, sync model, recovery procedure.
Steady state (2026-04-26 onward): both feature branches we used to
carry have merged upstream (PRs NousResearch#13445 and NousResearch#14904 via NousResearch#15050 and
NousResearch#15191). FEATURE_BRANCHES is now empty; main collapses to
upstream/main + ops-overlay. The fork stays alive so customer VPSes
have a stable deploy target rebuilt on our schedule.
github-actions Bot
pushed a commit
to pebble-tech/hermes-agent
that referenced
this pull request
May 6, 2026
This is the sole commit on ops-overlay. It carries:
- .github/workflows/sync-upstream.yml: daily rebase + rebuild of main
on top of upstream/main + ops-overlay + each feature branch.
- FORK.md: branch layout, sync model, recovery procedure.
Steady state (2026-04-26 onward): both feature branches we used to
carry have merged upstream (PRs NousResearch#13445 and NousResearch#14904 via NousResearch#15050 and
NousResearch#15191). FEATURE_BRANCHES is now empty; main collapses to
upstream/main + ops-overlay. The fork stays alive so customer VPSes
have a stable deploy target rebuilt on our schedule.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
On WhatsApp, the bridge routinely hands the gateway two different JIDs for the same contact within the same conversation — the LID form (
999999999999999@lid) and the phone form (15551234567@s.whatsapp.net) — and flips between the two. Becausebuild_session_keyused raw identifiers verbatim, the flip produced two distinct session keys for the same human in two places:chat_id— a user's DM sessions split in half; transcripts and per-sender state diverge.participant_id(withgroup_sessions_per_user=True) — a member's per-user session inside a group splits in half for the same reason.This is a different concern from the existing
_expand_whatsapp_auth_aliaseshelper ingateway/run.py, which consults the samelid-mapping-*.jsonbridge files only for authorisation. Session keying never ran through that mapping, so the split persisted even when auth succeeded.Fix
Collapse WhatsApp user-identity aliases into a single canonical identifier when building the session key. Three small helpers are added to
gateway/session.py:normalize_whatsapp_identifier(public) — strips@lid,@s.whatsapp.net, device suffix (:N), and leading+down to the bare numeric form._expand_whatsapp_aliases(private) — walks the bridge's$HERMES_HOME/whatsapp/session/lid-mapping-<id>{,_reverse}.jsonfiles (same mapping the auth path already uses) to resolve LID/phone pairs to a single set.canonical_whatsapp_identifier(public) — picks the shortest identifier (ties broken lexicographically) as the stable key component.build_session_keynow routes both the WhatsApp DMchat_idand the WhatsApp groupparticipant_idthroughcanonical_whatsapp_identifier. All other platforms and chat types are untouched.Result: both JID shapes produce the same key, e.g.
agent:main:whatsapp:dm:15551234567agent:main:whatsapp:group:120363…@g.us:15551234567so the transcript is shared across alias flips.
Why expose two of the helpers
Plugins that need per-sender behaviour on WhatsApp (role-based routing, per-contact authorization, policy gating in a gateway hook) must resolve the same JID↔LID aliases the core already resolves — otherwise their bookkeeping keys drift apart from Hermes' session keys. Without a public helper, each such plugin would have to reimplement the walker against the bridge's internal on-disk format.
Exposing
canonical_whatsapp_identifierandnormalize_whatsapp_identifierkeeps the resolution authoritative in one place._expand_whatsapp_aliasesstays private — it's an implementation detail of how the mapping files are walked, not a contract callers should depend on.Test plan
TestWhatsAppSessionKeyConsistency::test_whatsapp_dm_aliases_share_one_session_key— DM LID and phone JID share one key.TestWhatsAppSessionKeyConsistency::test_whatsapp_group_participant_aliases_share_session_key— group participant LID and phone JID share one per-user key whengroup_sessions_per_user=True.TestWhatsAppSessionKeyConsistency::test_whatsapp_group_shared_sessions_untouched_by_canonicalisation—group_sessions_per_user=Falsepath is a no-op for canonicalisation (participant isn't in the key anyway).TestWhatsAppIdentifierPublicHelpers— contract tests for the public helpers (JID/LID/device-suffix/plus-prefix stripping, transitive mapping walk, empty input).tests/gateway/test_session.py— 75/75 pass locally.group_sessions_per_user-dependent suites (test_config.py,test_slack.py,test_shared_group_sender_prefix.py) — 160/160 pass locally.Notes for reviewers
normalize_whatsapp_identifier/_expand_whatsapp_aliasesalready exist ingateway/run.pyfor auth-alias matching. They are kept locally ingateway/session.pyhere to keep the diff small; happy to extract both usages into a shared module (e.g.gateway/whatsapp_identity.py) as a follow-up if you prefer — the helpers can't be imported directly fromgateway.runbecauserun.pyalready imports fromgateway.session.website/docs/user-guide/sessions.mdis updated to reflect the new DM key shape.@...off the raw identifier, so there is no migration path needed for existing sessions other than the normal drift that already happens when a user's JID form changes.