Skip to content

Comments

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

Open
NOVA-Openclaw wants to merge 1 commit intoopenclaw:mainfrom
NOVA-Openclaw:feature/message-hooks-v2
Open

feat(hooks): add message:received and message:sent hook events#19922
NOVA-Openclaw wants to merge 1 commit intoopenclaw:mainfrom
NOVA-Openclaw:feature/message-hooks-v2

Conversation

@NOVA-Openclaw
Copy link

@NOVA-Openclaw NOVA-Openclaw commented Feb 18, 2026

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.tsRegression 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 d5e25e0 ("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 Summary

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.

Confidence Score: 2/5

  • 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).

Last reviewed commit: 871a678

(5/5) You can turn off certain types of comments like style here!

Context used:

  • Context from dashboard - CLAUDE.md (source)

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.

7 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +57 to +68
const hookEvent = createInternalHookEvent("message", "received", sessionKey, {
message: ctx.Body ?? "",
rawBody: ctx.RawBody,
senderId: ctx.SenderId,
senderName: ctx.SenderName,
channel: ctx.Provider,
messageId: ctx.MessageSid,
isGroup: ctx.ChatType === "group",
groupId: ctx.ChatType === "group" ? ctx.From : undefined,
timestamp: ctx.Timestamp,
commandAuthorized: ctx.CommandAuthorized,
} satisfies MessageReceivedContext);
Copy link
Contributor

Choose a reason for hiding this comment

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

Context shape conflicts with existing type guards

internal-hooks.ts already defines MessageReceivedHookContext (with fields from, content, channelId) and an isMessageReceivedEvent type guard that checks for those fields. However, the event created here uses a completely different context shape (message, senderId, channel via MessageReceivedContext).

Any consumer using isMessageReceivedEvent() to narrow the hook event will get false for events produced by triggerMessageReceived, because the guard checks typeof context.from === "string" && typeof context.channelId === "string" — neither of which exist in the MessageReceivedContext shape.

The same issue exists for triggerMessageSent vs isMessageSentEvent (which checks for to, channelId, success).

Either align the context shapes with the existing types in internal-hooks.ts, or update the type guards to handle the new shape.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/message-hooks.ts
Line: 57-68

Comment:
**Context shape conflicts with existing type guards**

`internal-hooks.ts` already defines `MessageReceivedHookContext` (with fields `from`, `content`, `channelId`) and an `isMessageReceivedEvent` type guard that checks for those fields. However, the event created here uses a completely different context shape (`message`, `senderId`, `channel` via `MessageReceivedContext`).

Any consumer using `isMessageReceivedEvent()` to narrow the hook event will get `false` for events produced by `triggerMessageReceived`, because the guard checks `typeof context.from === "string" && typeof context.channelId === "string"` — neither of which exist in the `MessageReceivedContext` shape.

The same issue exists for `triggerMessageSent` vs `isMessageSentEvent` (which checks for `to`, `channelId`, `success`).

Either align the context shapes with the existing types in `internal-hooks.ts`, or update the type guards to handle the new shape.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +166 to +172
if (options.hookContext?.sessionKey) {
await triggerMessageSent(options.hookContext.sessionKey, normalized, {
target: options.hookContext.target,
channel: options.hookContext.channel,
kind,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

message:sent hook never fires — no caller passes hookContext

The hookContext option was added to ReplyDispatcherOptions, but none of the existing callers of createReplyDispatcher or createReplyDispatcherWithTyping (in dispatch.ts, signal/monitor/event-handler.ts, slack/monitor/message-handler/dispatch.ts, discord/monitor/message-handler.process.ts, imessage/monitor/monitor-provider.ts, gateway/server-methods/chat.ts) actually pass hookContext. This means options.hookContext?.sessionKey will always be falsy and triggerMessageSent will never execute in production.

The channel dispatchers need to be wired to pass hookContext for this to work.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/reply-dispatcher.ts
Line: 166-172

Comment:
**`message:sent` hook never fires — no caller passes `hookContext`**

The `hookContext` option was added to `ReplyDispatcherOptions`, but none of the existing callers of `createReplyDispatcher` or `createReplyDispatcherWithTyping` (in `dispatch.ts`, `signal/monitor/event-handler.ts`, `slack/monitor/message-handler/dispatch.ts`, `discord/monitor/message-handler.process.ts`, `imessage/monitor/monitor-provider.ts`, `gateway/server-methods/chat.ts`) actually pass `hookContext`. This means `options.hookContext?.sessionKey` will always be falsy and `triggerMessageSent` will never execute in production.

The channel dispatchers need to be wired to pass `hookContext` for this to work.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +165 to +171
// Trigger message:sent hook after successful delivery
if (options.hookContext?.sessionKey) {
await triggerMessageSent(options.hookContext.sessionKey, normalized, {
target: options.hookContext.target,
channel: options.hookContext.channel,
kind,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Awaiting hook inside the delivery chain adds latency to all subsequent deliveries

triggerMessageSent is awaited inside the serialized sendChain. Since triggerInternalHook runs handlers sequentially with await, a slow hook handler will delay all queued message deliveries. Consider firing the hook without blocking the chain (e.g., void triggerMessageSent(...)) or running it in a .then() that doesn't gate the next delivery, depending on whether ordering guarantees are needed.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/reply-dispatcher.ts
Line: 165-171

Comment:
**Awaiting hook inside the delivery chain adds latency to all subsequent deliveries**

`triggerMessageSent` is awaited inside the serialized `sendChain`. Since `triggerInternalHook` runs handlers sequentially with `await`, a slow hook handler will delay all queued message deliveries. Consider firing the hook without blocking the chain (e.g., `void triggerMessageSent(...)`) or running it in a `.then()` that doesn't gate the next delivery, depending on whether ordering guarantees are needed.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Add hook events for the message lifecycle, enabling workspace hooks
to trigger on incoming and outgoing messages (e.g., memory extraction,
semantic recall, logging).

- Create src/hooks/message-hooks.ts with triggerMessageReceived and
  triggerMessageSent helpers
- Trigger message:received in dispatch.ts before agent processing
- Trigger message:sent in reply-dispatcher.ts after successful delivery
- Add hookContext option to ReplyDispatcherOptions for session/channel context
- Export message hook types from hooks.ts
- Add unit tests for both hook triggers
- Add regression test ensuring dispatchInboundMessage calls
  triggerMessageReceived (prevents silent removal by future refactors)

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Implement message:received hook event

1 participant