feat(hooks): add message:received and message:sent hook events#19922
feat(hooks): add message:received and message:sent hook events#19922NOVA-Openclaw wants to merge 1 commit intoopenclaw:mainfrom
Conversation
| 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); |
There was a problem hiding this 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.
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.| if (options.hookContext?.sessionKey) { | ||
| await triggerMessageSent(options.hookContext.sessionKey, normalized, { | ||
| target: options.hookContext.target, | ||
| channel: options.hookContext.channel, | ||
| kind, | ||
| }); | ||
| } |
There was a problem hiding this 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.
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.| // 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, | ||
| }); |
There was a problem hiding this 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.
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
871a678 to
d55a640
Compare
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 withtriggerMessageReceivedandtriggerMessageSenthelperssrc/auto-reply/dispatch.ts— Wiremessage:receivedtrigger before agent processing indispatchInboundMessage()src/auto-reply/reply/reply-dispatcher.ts— Wiremessage:senttrigger after successful delivery; addhookContextoption toReplyDispatcherOptionssrc/hooks/hooks.ts— Export message hook typessrc/hooks/message-hooks.test.ts— Unit tests for both triggerssrc/auto-reply/dispatch.test.ts— Regression test ensuringdispatchInboundMessagecallstriggerMessageReceived(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") whendispatch.tswas rewritten. This went undetected because there was no regression test.This updated PR:
mainwithReplyDispatcherpattern indispatch.tsHook Events
message:receiveddispatchInboundMessage()message:sentcreateReplyDispatcher()delivery callbackUse Cases
Closes #5053
Greptile Summary
This PR adds
message:receivedandmessage:senthook events to the internal hook system, enabling workspace hooks to trigger on inbound and outbound messages. Themessage:receivedhook is wired intodispatchInboundMessage()and fires correctly before agent processing. A regression test is included to prevent the call site from being silently dropped again.Key issues found:
MessageReceivedContextandMessageSentContexttypes inmessage-hooks.tsuse different field names (message,senderId,channel,text,target) than the existingMessageReceivedHookContextandMessageSentHookContexttypes ininternal-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:senthook is dead code in production: ThehookContextoption was added toReplyDispatcherOptions, but no channel dispatcher (Signal, Slack, Discord, iMessage, webchat) passes it. Themessage:senthook will never fire until callers are wired up..gitignoreaddition foropenclaw-*.tgzis unrelated housekeeping.Confidence Score: 2/5
src/hooks/message-hooks.ts(conflicting context types) andsrc/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:
dashboard- CLAUDE.md (source)