#20858: fix(hooks): normalize hook addresses to canonical provider:kind:id format
agents
size: L
trusted-contributor
## Summary
Extends the existing `${provider}:${kind}:${finalId}` pattern from `resolveGroupSessionKey` (src/config/sessions/group.ts) to hook event payloads (`message_received`, `message_sent`).
Currently, hook consumers receive raw, inconsistent address formats — bare snowflakes, `channel:` prefixed IDs, `user:` prefixed IDs, or partially-qualified `telegram:999` — depending on the provider and chat type. This makes it impossible for plugins to reliably key on channel identity without re-implementing parsing logic.
This PR normalizes all hook address fields to the canonical `provider:kind:id` format (e.g. `discord:channel:123`, `telegram:user:999`, `whatsapp:group:123@g.us`), consistent with the vocabulary already used in upstream's group session key resolution.
## Changes
- **`toCanonicalAddress()`** — new function that normalizes any raw address + context into `provider:kind:id` format
- **`parseRoutingAddress()`** — new parser that extracts `{ provider?, kind?, id }` from any address format
- **`chatTypeToKind()`** — maps chatType values (`direct`/`group`/`channel`/`supergroup`) to canonical kind (`user`/`group`/`channel`)
- Applied to `from`, `to`, `originatingTo`, and `conversationId` fields in the `message_received` hook payload
- Guards against null/undefined/empty/object-typed inputs
- Idempotent — already-canonical addresses pass through unchanged
- Naming aligned with upstream's `provider`/`kind`/`id` vocabulary
## Test plan
- [x] 41 tests covering `parseRoutingAddress`, `toCanonicalAddress`, and integration with `dispatchReplyFromConfig`
- [x] Discord guild → `discord:channel:<snowflake>`
- [x] Discord DM → `discord:user:<snowflake>`
- [x] Telegram DM/group → `telegram:user:<id>` / `telegram:group:<id>`
- [x] WhatsApp DM/group → `whatsapp:user:<e164>` / `whatsapp:group:<jid>`
- [x] Signal DM/group → `signal:user:<e164>` / `signal:group:<id>`
- [x] Slack channel → `slack:channel:<id>`
- [x] iMessage DM → `imessage:user:<id>`
- [x] Bare snowflakes → `unknown:user:<id>`
- [x] Null/undefined/empty/whitespace → `undefined`
- [x] Idempotency — already-canonical addresses preserved
---
_Re-opened from #15824 (auto-closed during fork maintenance)._
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR introduces address normalization for hook event payloads, standardizing all routing addresses to the canonical `provider:kind:id` format (e.g. `discord:channel:123`, `telegram:user:999`). This aligns with the existing pattern in `resolveGroupSessionKey` and ensures hook consumers receive consistent address formats across all messaging platforms.
**Key changes:**
- Added `parseRoutingAddress()` to parse addresses in various formats (`provider:kind:id`, `provider:id`, `kind:id`, or bare `id`)
- Added `toCanonicalAddress()` to normalize any address format to canonical form using context (provider, chatType)
- Added `chatTypeToKind()` helper to map chatType values to canonical kinds
- Applied normalization to `from`, `to`, `originatingTo`, and `conversationId` fields in `message_received` hook payloads
- Added `guildId` and `channelName` metadata fields to hook payloads
- Comprehensive test coverage (41 tests) for parsing, normalization, and integration
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk
- The implementation is well-designed with comprehensive test coverage (41 tests), handles edge cases properly (null/undefined/empty/whitespace), and maintains backward compatibility through idempotency. The normalization logic is defensive and falls back gracefully when context is missing.
- No files require special attention
<sub>Last reviewed commit: 870dbf7</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#5525: Add plugin hook to resolve canonical RoomKey for session identity a...
by peteclt92 · 2026-01-31
76.4%
#20859: fix(hooks): wire message_sent hook into reply dispatcher for all ch...
by davidrudduck · 2026-02-19
75.4%
#14746: fix(hooks): use globalThis for handler registry to survive bundler ...
by openperf · 2026-02-12
73.4%
#9906: feat: wire message_sending hook in outbound delivery
by teempai · 2026-02-05
73.0%
#14888: fix(routing): normalize peer.kind in matchesPeer for symmetric comp...
by omair445 · 2026-02-12
73.0%
#8084: fix(plugins): wire up message_sending hook in outbound delivery
by lailoo · 2026-02-03
72.9%
#19922: feat(hooks): add message:received and message:sent hook events
by NOVA-Openclaw · 2026-02-18
72.8%
#23019: fix(hooks): use globalThis singleton for internal hooks handlers Map
by karmafeast · 2026-02-21
72.7%
#17878: Refactor: share allowlist normalization
by iyoda · 2026-02-16
72.4%
#13489: fix: preserve Slack channel/user ID case in target normalization
by sandieman2 · 2026-02-10
72.3%