#19922: feat(hooks): add message:received and message:sent hook events
size: M
## Summary
Add hook events for the message lifecycle, enabling workspace hooks to trigger on incoming and outgoing messages (e.g., memory extraction, semantic recall, logging).
### Changes
- **`src/hooks/message-hooks.ts`** — New file with `triggerMessageReceived` and `triggerMessageSent` helpers
- **`src/auto-reply/dispatch.ts`** — Wire `message:received` trigger before agent processing in `dispatchInboundMessage()`
- **`src/auto-reply/reply/reply-dispatcher.ts`** — Wire `message:sent` trigger after successful delivery; add `hookContext` option to `ReplyDispatcherOptions`
- **`src/hooks/hooks.ts`** — Export message hook types
- **`src/hooks/message-hooks.test.ts`** — Unit tests for both triggers
- **`src/auto-reply/dispatch.test.ts`** — **Regression test** ensuring `dispatchInboundMessage` calls `triggerMessageReceived` (prevents silent removal by future refactors)
### Context
Supersedes #6797. The original PR was merged into our fork but the `triggerMessageReceived()` call site was silently dropped by commit d5e25e0ad ("refactor: centralize dispatcher lifecycle ownership") when `dispatch.ts` was rewritten. This went undetected because there was no regression test.
This updated PR:
1. Rebases cleanly onto current `main`
2. Adapts to the new `withReplyDispatcher` pattern in `dispatch.ts`
3. Adds a dedicated regression test to prevent recurrence
### Hook Events
| Event | Fires when | Location |
|---|---|---|
| `message:received` | Inbound message about to be processed | `dispatchInboundMessage()` |
| `message:sent` | Outbound reply successfully delivered | `createReplyDispatcher()` delivery callback |
### Use Cases
- Memory extraction from incoming messages
- Semantic recall injection into agent context
- Activity tracking and logging
- Message analytics
Closes #5053
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
This PR adds `message:received` and `message:sent` hook events to the internal hook system, enabling workspace hooks to trigger on inbound and outbound messages. The `message:received` hook is wired into `dispatchInboundMessage()` and fires correctly before agent processing. A regression test is included to prevent the call site from being silently dropped again.
**Key issues found:**
- **Conflicting context shapes:** The new `MessageReceivedContext` and `MessageSentContext` types in `message-hooks.ts` use different field names (`message`, `senderId`, `channel`, `text`, `target`) than the existing `MessageReceivedHookContext` and `MessageSentHookContext` types in `internal-hooks.ts` (`from`, `content`, `channelId`, `to`, `success`). The existing type guards (`isMessageReceivedEvent`, `isMessageSentEvent`) will **never match** events produced by the new triggers, which could break consumers relying on those guards.
- **`message:sent` hook is dead code in production:** The `hookContext` option was added to `ReplyDispatcherOptions`, but no channel dispatcher (Signal, Slack, Discord, iMessage, webchat) passes it. The `message:sent` hook will never fire until callers are wired up.
- The `.gitignore` addition for `openclaw-*.tgz` is unrelated housekeeping.
<h3>Confidence Score: 2/5</h3>
- This PR has functional issues — conflicting type shapes break existing type guards, and the message:sent hook is effectively dead code.
- Score of 2 reflects two significant issues: (1) the new hook context shapes are incompatible with existing type guards in internal-hooks.ts, which will cause isMessageReceivedEvent/isMessageSentEvent to return false for events from these triggers, and (2) the message:sent hook is never triggered in production because no dispatcher caller passes hookContext.
- Pay close attention to `src/hooks/message-hooks.ts` (conflicting context types) and `src/auto-reply/reply/reply-dispatcher.ts` (unwired hookContext option).
<sub>Last reviewed commit: 871a678</sub>
<!-- greptile_other_comments_section -->
<sub>(5/5) You can turn off certain types of comments like style [here](https://app.greptile.com/review/github)!</sub>
**Context used:**
- Context from `dashboard` - CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=fd949e91-5c3a-4ab5-90a1-cbe184fd6ce8))
<!-- /greptile_comment -->
Most Similar PRs
#11597: feat(hooks): implement message:received hook
by gnufoo · 2026-02-08
88.7%
#7545: feat(hooks): add message:received hook for pre-turn automation
by wangtian24 · 2026-02-02
87.7%
#7580: feat: add message:received internal hook with prompt injection
by rodrigoschott · 2026-02-03
87.3%
#10109: feat(plugins): invoke message_received and message_sent hooks
by nezovskii · 2026-02-06
86.1%
#20859: fix(hooks): wire message_sent hook into reply dispatcher for all ch...
by davidrudduck · 2026-02-19
84.7%
#19565: feat: add agent lifecycle hook events (session, message, error)
by tag-assistant · 2026-02-17
84.6%
#16618: feat: bridge message lifecycle hooks to workspace hook system
by DarlingtonDeveloper · 2026-02-14
84.0%
#10706: feat(hooks): add message:received internal hook for Telegram
by thebtf · 2026-02-06
82.9%
#15577: feat(hooks): add message:preprocessed hook event
by heybeaux · 2026-02-13
82.3%
#6630: feat(hooks): add agent:turn_start and agent:turn_end lifecycle events
by drdigital13 · 2026-02-01
80.9%