Skip to content

Comments

feat(hooks): add message:received and message:sent internal hook events#14196

Closed
grunt3714-lgtm wants to merge 2 commits intoopenclaw:mainfrom
grunt3714-lgtm:feat/message-hooks
Closed

feat(hooks): add message:received and message:sent internal hook events#14196
grunt3714-lgtm wants to merge 2 commits intoopenclaw:mainfrom
grunt3714-lgtm:feat/message-hooks

Conversation

@grunt3714-lgtm
Copy link

@grunt3714-lgtm grunt3714-lgtm commented Feb 11, 2026

Summary

Implements the message:received and message:sent internal 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 processed

  • Context: text, channel, chatType, from, to, senderId, senderName, senderUsername, messageId, threadId, timestamp, accountId, mediaUrls, mediaTypes
  • Triggered in dispatchReplyFromConfig() after duplicate check

message:sent — Triggered after an outbound message is successfully delivered

  • Context: text, channel, chatType, kind (tool/block/final), mediaUrl, mediaUrls
  • Triggered in the reply dispatcher after successful delivery

Implementation

  1. Added "message" to InternalHookEventType (src/hooks/internal-hooks.ts)
  2. Trigger message:received in dispatchReplyFromConfig() for inbound messages (src/auto-reply/reply/dispatch-from-config.ts)
  3. Trigger message:sent in the reply dispatcher after delivery success (src/auto-reply/reply/reply-dispatcher.ts)
  4. Added sessionKey, channel, chatType to ReplyDispatcherOptions for hook context (src/auto-reply/reply/reply-dispatcher.ts)
  5. Inject session context into dispatcher options in both dispatchers (src/auto-reply/dispatch.ts)
  6. Updated docs to move these events from "Future Events" to documented (docs/automation/hooks.md)

Tests

6 test cases in src/hooks/message-hooks.test.ts:

  • Event triggering for message:received and message:sent
  • General (message) and specific (message:received) handler routing
  • Media field inclusion
  • Error isolation (handler errors do not propagate)
  • Type validation

Use Cases

  • Conversation logging — persist all messages to a database for analytics or fine-tuning
  • Memory systems — feed messages into RAG/retrieval pipelines
  • Archival & search — index conversations for full-text search
  • Output validation — inspect outbound messages before/after delivery

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 message and emits two lifecycle events: message:received from the inbound message dispatcher and message:sent after 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:sent is emitted inside createReplyDispatcher() for all dispatchers, but the hook context relies on optional ReplyDispatcherOptions fields (sessionKey, channel, chatType). Some existing call sites (e.g. gateway webchat) construct a dispatcher without these fields, so emitted events can have an empty sessionKey and undefined channel/chatType, which makes the new event unreliable for consumers and inconsistent with the documentation.

Confidence Score: 3/5

  • This PR is close to mergeable but needs a fix to ensure message hook events have consistent context across all dispatcher call sites.
  • The core implementation and tests look sound, but message:sent is 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.
  • src/auto-reply/reply/reply-dispatcher.ts (hook emission relies on optional context); src/gateway/server-methods/chat.ts (dispatcher constructed without new context fields)

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

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
@openclaw-barnacle openclaw-barnacle bot added the docs Improvements or additions to documentation label Feb 11, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 153 to 164
// 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)}`);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Author

Choose a reason for hiding this comment

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

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.
@Drickon
Copy link

Drickon commented Feb 11, 2026

Hey, nice work pulling these together. Heads up that #9859 also covers a message:transcribed event for audio workflows if that's on your radar.

@matskevich
Copy link

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: message:received fires before applyMediaUnderstanding(), so voice messages arrive as <media:audio> with no transcript. photos = <media:image>, no description. any hook that needs actual content (semantic logging, moderation, search) gets useless tags.

opened a discussion about this: #14539

proposal: add message:preprocessed — fires after media/link processing, before agent. ~40 lines, 2 files. gives hooks access to transcripts, image descriptions, and processed body.

also — groupId needs to be in MessageSentContext (same as received). without it, received and sent events for the same conversation get different identifiers. documented here: #6797 (comment)

happy to submit either fix as a follow-up PR if this lands first.

matskevich added a commit to matskevich/openclaw-infra that referenced this pull request Feb 13, 2026
- 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]>
@grunt3714-lgtm
Copy link
Author

Closing — message_received and message_sent hooks are now built into 2026.2.12 natively. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: message:sent and message:received hooks

3 participants