feat(hooks): add message:received and message:sent internal hook events#14196
feat(hooks): add message:received and message:sent internal hook events#14196grunt3714-lgtm wants to merge 2 commits intoopenclaw:mainfrom
Conversation
Implements the message lifecycle hooks documented as 'Future Events' in the hooks documentation. These events enable extensions to observe inbound and outbound messages for logging, analytics, memory systems, and archival. New events: - message:received: triggered when an inbound message is processed - message:sent: triggered after an outbound message is successfully delivered Both events include rich context (text, channel, chatType, sender info, media URLs) and are fired asynchronously so they never block message processing. Includes: - 6 test cases covering event triggering, general/specific handler routing, media fields, error isolation, and type validation - Documentation update moving these from 'Future Events' to documented events Combines approaches from prior community PRs openclaw#6384, openclaw#6797, and openclaw#9859. Resolves openclaw#12775, openclaw#8807, openclaw#13004 Related: openclaw#5354, openclaw#12867, openclaw#12914
| // Trigger message:sent hook after successful delivery | ||
| const hookEvent = createInternalHookEvent("message", "sent", options.sessionKey ?? "", { | ||
| text: normalized.text, | ||
| channel: options.channel, | ||
| chatType: options.chatType, | ||
| kind, | ||
| mediaUrl: normalized.mediaUrl, | ||
| mediaUrls: normalized.mediaUrls, | ||
| }); | ||
| void triggerInternalHook(hookEvent).catch((err) => { | ||
| logVerbose(`reply-dispatcher: message:sent hook failed: ${String(err)}`); | ||
| }); |
There was a problem hiding this comment.
Missing hook context fields
createReplyDispatcher() now always emits message:sent, but the event context depends on options.sessionKey/channel/chatType, which are optional and not set by all call sites. For example, the webchat path creates a dispatcher without these fields (src/gateway/server-methods/chat.ts:502), so hooks will see an empty sessionKey and channel/chatType as undefined, contradicting the docs and making the event hard to consume reliably. Consider either (a) requiring these fields when internal hooks are enabled/emitting, or (b) deriving them from the payload/context at the dispatcher boundary so all senders produce consistent hook context.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/reply-dispatcher.ts
Line: 153:164
Comment:
**Missing hook context fields**
`createReplyDispatcher()` now always emits `message:sent`, but the event context depends on `options.sessionKey/channel/chatType`, which are optional and not set by all call sites. For example, the webchat path creates a dispatcher without these fields (`src/gateway/server-methods/chat.ts:502`), so hooks will see an empty `sessionKey` and `channel/chatType` as `undefined`, contradicting the docs and making the event hard to consume reliably. Consider either (a) requiring these fields when internal hooks are enabled/emitting, or (b) deriving them from the payload/context at the dispatcher boundary so all senders produce consistent hook context.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Good catch — fixed in 3835023. The message:sent hook now only fires when sessionKey is present, so call sites without session context (like webchat) skip the hook rather than emitting incomplete events. Also defaulting channel/chatType to "unknown" when unset.
Only emit the message:sent hook when sessionKey is available. Call sites that don't inject session context (e.g., webchat dispatcher) will skip the hook rather than emitting events with empty/undefined fields. Defaults channel and chatType to 'unknown' when present but unset, ensuring consumers always see string values. Addresses review feedback from greptile-apps.
|
Hey, nice work pulling these together. Heads up that #9859 also covers a |
|
this is great. been running message:received + message:sent in production for 7+ days (550+ events, 2 bots, 4 groups) — works well. one thing we hit: opened a discussion about this: #14539 proposal: add also — happy to submit either fix as a follow-up PR if this lands first. |
- hooks/memory-logger: raw log hook (received/preprocessed/sent → jsonl) - packages/message-hooks: distributable patch + handler for openclaw - patches/message-hooks-pr6797: backport patch + install guide + results - docs/memory: full architecture (5 layers, write/read paths, frameworks) - docs/memory/share-pack.md: one-file briefing with all public links related: openclaw/openclaw#14196, Discussion #14539 Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
Closing — message_received and message_sent hooks are now built into 2026.2.12 natively. Thanks! |
Summary
Implements the
message:receivedandmessage:sentinternal hook events documented as "Future Events" in the hooks documentation. These events enable extensions to observe the complete message lifecycle for logging, analytics, memory systems, conversation archival, and search indexing.Changes
New Events
message:received— Triggered when an inbound message is processedtext,channel,chatType,from,to,senderId,senderName,senderUsername,messageId,threadId,timestamp,accountId,mediaUrls,mediaTypesdispatchReplyFromConfig()after duplicate checkmessage:sent— Triggered after an outbound message is successfully deliveredtext,channel,chatType,kind(tool/block/final),mediaUrl,mediaUrlsImplementation
"message"toInternalHookEventType(src/hooks/internal-hooks.ts)message:receivedindispatchReplyFromConfig()for inbound messages (src/auto-reply/reply/dispatch-from-config.ts)message:sentin the reply dispatcher after delivery success (src/auto-reply/reply/reply-dispatcher.ts)sessionKey,channel,chatTypetoReplyDispatcherOptionsfor hook context (src/auto-reply/reply/reply-dispatcher.ts)src/auto-reply/dispatch.ts)docs/automation/hooks.md)Tests
6 test cases in
src/hooks/message-hooks.test.ts:message:receivedandmessage:sentmessage) and specific (message:received) handler routingUse Cases
Prior Work
This PR combines approaches from the community PRs below. It takes the minimal footprint of #6384, adds tests inspired by #6797, and includes the docs update from #9859.
Related Issues
Resolves #12775, #8807, #13004
Related: #5354, #12867, #12914
Greptile Overview
Greptile Summary
This PR adds new internal hook event type
messageand emits two lifecycle events:message:receivedfrom the inbound message dispatcher andmessage:sentafter successful outbound delivery. It also threads session/channel/chatType context into the auto-reply dispatch path and updates hooks documentation accordingly, with a small Vitest suite to validate handler routing and error isolation.One issue to address before merge:
message:sentis emitted insidecreateReplyDispatcher()for all dispatchers, but the hook context relies on optionalReplyDispatcherOptionsfields (sessionKey,channel,chatType). Some existing call sites (e.g. gateway webchat) construct a dispatcher without these fields, so emitted events can have an emptysessionKeyand undefined channel/chatType, which makes the new event unreliable for consumers and inconsistent with the documentation.Confidence Score: 3/5
message:sentis emitted from a shared dispatcher that is also used in paths that don't supply the new optional context fields, leading to incomplete hook events (empty sessionKey / undefined channel/chatType) in real usage.(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!