Skip to content

Comments

feat(hooks): add message:transcribed and message:preprocessed internal hooks#9859

Open
Drickon wants to merge 2 commits intoopenclaw:mainfrom
Drickon:feature/message-hooks
Open

feat(hooks): add message:transcribed and message:preprocessed internal hooks#9859
Drickon wants to merge 2 commits intoopenclaw:mainfrom
Drickon:feature/message-hooks

Conversation

@Drickon
Copy link

@Drickon Drickon commented Feb 5, 2026

Summary

Adds two new internal hook events that fire after media and link understanding:

message:transcribed

Fires when audio has been fully transcribed. Provides the transcript text alongside the original body and media metadata. Useful for logging, analytics, or routing based on spoken content.

message:preprocessed

Fires for every message after all media + link understanding completes — giving hooks access to the fully enriched body (transcripts, image descriptions, link summaries) before the agent sees it.

Context

message:received and message:sent were backported by the upstream team in f07bb8e (merged Feb 17). This PR adds the two remaining hooks from the original set.

Community

  • 🙋 @j0nscalet has been waiting on message:transcribed for a voice-note logging integration
  • @matskevich is running message:preprocessed in production on a fork and reports it stable

Changes

  • src/auto-reply/reply/get-reply.ts — triggers message:transcribed (audio only) and message:preprocessed (all messages) after applyMediaUnderstanding + applyLinkUnderstanding
  • src/hooks/internal-hooks.ts — adds MessageTranscribedHookContext, MessagePreprocessedHookContext, and corresponding event types + type guards
  • src/hooks/message-hooks.test.ts — new test file covering all four message event types
  • docs/automation/hooks.md — documents all four message events and their contexts
  • src/config/types.hooks.ts — updates comment to include new event keys

@openclaw-barnacle openclaw-barnacle bot added the docs Improvements or additions to documentation label Feb 5, 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.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 132 to 155
// Trigger message:transcribed hook after media understanding completes
// At this point, audio has been transcribed and Transcript field is populated
await triggerInternalHook(
createInternalHookEvent("message", "transcribed", finalized.SessionKey ?? "", {
from: finalized.From,
to: finalized.To,
body: finalized.Body,
bodyForAgent: finalized.BodyForAgent,
transcript: finalized.Transcript,
timestamp: finalized.Timestamp,
channelId: finalized.ChannelId,
conversationId: finalized.ConversationId,
messageId: finalized.MessageSid,
senderId: finalized.SenderId,
senderName: finalized.SenderName,
senderUsername: finalized.SenderUsername,
provider: finalized.Provider,
surface: finalized.Surface,
mediaPath: finalized.MediaPath,
mediaType: finalized.MediaType,
cfg,
}),
);

Copy link
Contributor

Choose a reason for hiding this comment

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

Hook fires in tests

message:transcribed is triggered unconditionally even when OPENCLAW_TEST_FAST==="1" skips applyMediaUnderstanding/applyLinkUnderstanding (src/auto-reply/reply/get-reply.ts:119-155). That means in fast tests the hook will fire without any transcription/link processing having happened, contradicting the event’s meaning and docs. Gate this hook behind the same !isFastTestEnv condition or otherwise ensure it only fires after the processing steps actually ran.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/get-reply.ts
Line: 132:155

Comment:
**Hook fires in tests**

`message:transcribed` is triggered unconditionally even when `OPENCLAW_TEST_FAST==="1"` skips `applyMediaUnderstanding`/`applyLinkUnderstanding` (`src/auto-reply/reply/get-reply.ts:119-155`). That means in fast tests the hook will fire without any transcription/link processing having happened, contradicting the event’s meaning and docs. Gate this hook behind the same `!isFastTestEnv` condition or otherwise ensure it only fires after the processing steps actually ran.

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 on the original concern, but this is gated by the transcript check:

if (finalized.Transcript) {
  void triggerInternalHook(
    createInternalHookEvent("message", "transcribed", ...)
  ).catch(...);
}

In fast test mode, applyMediaUnderstanding is skipped, so finalized.Transcript remains falsy and the hook never fires. The guard ensures message:transcribed only triggers when transcription actually occurred.

Comment on lines 201 to 221
// Trigger internal hook for message:received
const messageEvent = createInternalHookEvent("message", "received", ctx.SessionKey ?? "", {
from: ctx.From,
to: ctx.To,
content,
timestamp,
channelId,
conversationId,
messageId: messageIdForHook,
senderId: ctx.SenderId,
senderName: ctx.SenderName,
senderUsername: ctx.SenderUsername,
senderE164: ctx.SenderE164,
provider: ctx.Provider,
surface: ctx.Surface,
threadId: ctx.MessageThreadId,
originatingChannel: ctx.OriginatingChannel,
originatingTo: ctx.OriginatingTo,
});
await triggerInternalHook(messageEvent);

Copy link
Contributor

Choose a reason for hiding this comment

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

Hook can block pipeline

dispatchReplyFromConfig now does await triggerInternalHook(messageEvent) for message:received (src/auto-reply/reply/dispatch-from-config.ts:201-221). Unlike the existing plugin hook runner call (which is fire-and-forget), this will delay message handling for every inbound message whenever any internal message/message:received hook is registered, and can also throw and fail the whole dispatch path if a handler rejects. If these hooks are intended to be “critical points” but non-blocking like the other hook system, consider making this non-blocking (or explicitly isolating errors) so user message processing isn’t gated on hook runtime/failures.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/dispatch-from-config.ts
Line: 201:221

Comment:
**Hook can block pipeline**

`dispatchReplyFromConfig` now does `await triggerInternalHook(messageEvent)` for `message:received` (`src/auto-reply/reply/dispatch-from-config.ts:201-221`). Unlike the existing plugin hook runner call (which is fire-and-forget), this will delay message handling for every inbound message whenever any internal `message`/`message:received` hook is registered, and can also throw and fail the whole dispatch path if a handler rejects. If these hooks are intended to be “critical points” but non-blocking like the other hook system, consider making this non-blocking (or explicitly isolating errors) so user message processing isn’t gated on hook runtime/failures.

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.

Thanks for flagging! This is actually already addressed in the current code — the hook is fired as fire-and-forget with error isolation:

void triggerInternalHook(messageEvent).catch((err) => {
  logVerbose(\`dispatch-from-config: message:received internal hook failed: ${String(err)}\`);
});

The void prefix ensures it does not block the dispatch pipeline, and the .catch() prevents unhandled rejections from propagating. Message processing continues immediately without waiting for hook completion.

@Zjianru
Copy link

Zjianru commented Feb 7, 2026

Strong support + use case: Event-driven agent notifications

强烈支持 + 使用场景:事件驱动的 Agent 主动通知


This PR addresses a critical gap. I built an entire external events-framework (1500 lines of Python: event inbox → router → rate-limit/dedupe/quiet-hours → Telegram) just because OpenClaw has no native event-driven hook for "something happened → notify user."

这个 PR 解决了一个关键缺口。我构建了一整套外部 events-framework(1500 行 Python:事件收件箱 → 路由器 → 限频/去重/静默时段 → Telegram),仅仅因为 OpenClaw 没有原生的事件驱动钩子。

My use case / 我的使用场景

I run a multi-agent system (1 PM + 8 sub-agents). When sub-agents complete work, cost thresholds are hit, or errors occur, I need immediate, deduplicated, rate-limited notifications — not cron polling every N minutes.

我运行一个多 Agent 系统(1 个项目经理 + 8 个子 Agent)。当子 Agent 完成工作、费用阈值触发或发生错误时,我需要即时、去重、限频的通知——而不是 cron 每 N 分钟轮询。

The message:received hook is the foundation for making OpenClaw truly event-driven rather than poll-driven.

message:received 钩子是让 OpenClaw 从轮询驱动变为事件驱动的基础。

On the Greptile performance concern / 关于 Greptile 的性能顾虑

Greptile flagged that message:received is awaited in the hot path. Hooks should be fire-and-forget to avoid blocking:

triggerInternalHook(event).catch(err => logger.warn("hook error", err));

Note on competing PRs / 关于竞争 PR

I see #7545 and #9387 also implement similar functionality. It would be great if maintainers could signal which approach they prefer so the community can consolidate effort.

我注意到 #7545#9387 也实现了类似功能。希望维护者能表明倾向哪种方案,让社区集中力量。

@Drickon
Copy link
Author

Drickon commented Feb 7, 2026

Thanks for the detailed support @Zjianru — great to see someone else hitting the same wall. Your event framework sounds like exactly the kind of thing that should be built-in rather than bolted on.

On the performance concern: the current code already uses fire-and-forget with error isolation (void triggerInternalHook(...).catch(...)) — addressed in a follow-up commit after the Greptile review. Hooks will not block the message pipeline.

Regarding the competing PRs (#7545, #9387) — here is how they compare:

#7545 #9387 #9859 (this PR)
message:received
message:sent
message:transcribed
All channels ❌ (WhatsApp only) ✅ (plugin bridge) ✅ (dispatch-level)
Non-blocking Unclear Plugin-coupled ✅ (fire-and-forget)
Modify/skip messages

This PR fires at the dispatch level (dispatch-from-config.ts) so it covers all channels without per-channel wiring. The message:transcribed hook is unique here — it fires after audio transcription completes, which is useful for workflows that need the actual transcript content (e.g., echoing transcripts back to the user before the agent responds).

Would love to see maintainers weigh in on which direction they prefer so effort can consolidate.

@Zjianru
Copy link

Zjianru commented Feb 8, 2026

Technical suggestions for the two missing capabilities / 关于两个缺失能力的技术建议

Hi @Drickon, thanks again for the thorough comparison table — it really clarifies the landscape. I noticed two gaps in this PR compared to the other approaches, and wanted to offer some concrete suggestions.

感谢你提供的详细对比表,非常清晰。我注意到相比其他方案,这个 PR 还有两个能力缺口,想提供一些具体的技术建议。


1. message:sent hook — recommended addition / 建议补充

Since this PR already fires hooks at the dispatch level (dispatch-from-config.ts), adding a message:sent hook at the dispatch outbound path should be a natural extension — same fire-and-forget pattern, just triggered after the agent's reply is delivered to the channel adapter.

既然这个 PR 已经在 dispatch 层触发 hooks,在 dispatch 出站路径 添加 message:sent hook 应该是自然延伸——同样的 fire-and-forget 模式,只是在 agent 回复投递到渠道适配器之后触发。

Use cases / 使用场景:

  • Audit logging (record every outbound message)
  • Post-reply workflows (e.g., update status, trigger follow-up tasks)
  • Analytics (response time tracking, message volume)

This would close the gap with #9387 while keeping the clean dispatch-level architecture.

这样可以补齐与 #9387 的差距,同时保持干净的 dispatch 层架构。


2. Message interception (modify/skip) — suggest keeping it separate / 建议作为独立 feature

The "modify/skip messages" capability from #7545 is fundamentally different from fire-and-forget hooks — it changes the hook semantics from notification to middleware/interceptor, which requires:

#7545 的"修改/跳过消息"能力与 fire-and-forget hooks 有本质区别——它将 hook 语义从通知变成了中间件/拦截器,这需要:

  • Synchronous waiting for hook return values (introduces latency)
  • Error handling for interceptor failures (what if the hook crashes — drop the message or pass through?)
  • Ordering guarantees when multiple interceptors are registered

I'd suggest not mixing this into the current PR. It deserves its own design discussion — perhaps as a separate message:intercept or message:beforeProcess hook with explicit opt-in semantics, so the fire-and-forget hooks remain lightweight and non-blocking.

建议不要混入当前 PR。这值得独立的设计讨论——也许作为单独的 message:interceptmessage:beforeProcess hook,使用显式的 opt-in 语义,让 fire-and-forget hooks 保持轻量和非阻塞。


Thanks for driving this forward — your dispatch-level approach is the cleanest of the three PRs in my view. Happy to contribute further if it helps move things along. 🙂

感谢你推动这件事——在我看来,你的 dispatch 层方案是三个 PR 中最干净的。如果能帮上忙,我很乐意为社区继续贡献。

@Drickon
Copy link
Author

Drickon commented Feb 8, 2026

Thanks for the suggestions! Added message:sent in the latest commit.

Agreed that interception/middleware should be a separate PR. Your multi-agent use case would give you better perspective on the design decisions there. If you're interested in taking that on, go for it. Just drop a comment here or open an issue so we can coordinate.

Appreciate the input!

@Drickon Drickon force-pushed the feature/message-hooks branch 2 times, most recently from 74ccda2 to 9a1eada Compare February 8, 2026 19:05
grunt3714-lgtm added a commit to grunt3714-lgtm/grunt3714-lgtm-openclaw that referenced this pull request Feb 11, 2026
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
@j0nscalet
Copy link

👋 Just wanted to add a +1 for this PR — we have a real use case waiting on it.

Use case: Auto-transcription of inbound audio messages via a workspace hook. Currently audio files arrive as attachments and can get lost in context compaction before processing. The message:transcribed hook would let us reliably capture transcripts for logging, memory, or forwarding to other channels.

The implementation looks solid (fire-and-forget, channel-agnostic, good test coverage). Would love to see this merged! 🙏

@Drickon Drickon changed the title feat(hooks): add message:received and message:transcribed internal hooks feat(hooks): add message:received, message:transcribed, message:preprocessed, and message:sent internal hooks Feb 13, 2026
@matskevich
Copy link

this is exactly what we proposed in #14539. been running all of these events (received + preprocessed + sent) in production for 8+ days — 550+ events, 2 bots, 4 groups, zero crashes.

a few things from production experience:

  1. groupId in message:sent — critical. without it, received and sent events for the same conversation get different identifiers, making correlation impossible. detailed writeup: feat(hooks): add message:received and message:sent hook events #6797 (comment)

  2. message:transcribed as separate event — smart decision. we initially combined it into preprocessed, but having a dedicated transcription event is cleaner for hooks that only care about audio (like @j0nscalet's use case).

  3. fire-and-forget + error isolation — confirmed working in production. hook errors never broke message delivery.

production hooks built on this:

  • memory logger (canonical append-only jsonl, feeds embeddings)
  • output DLP (regex + entropy scan on sent, catches leaked API keys)
  • multi-bot sync (events forwarded to shared hub, 2 bots see each other's conversations)

reference hook implementation: https://github.com/matskevich/openclaw-infra/tree/main/hooks/memory-logger

happy to help test or review.

…l hooks

Adds two new internal hook events that fire after media/link processing:

- message:transcribed: fires when audio has been transcribed, providing
  the transcript text alongside the original body and media metadata.
  Useful for logging, analytics, or routing based on spoken content.

- message:preprocessed: fires for every message after all media + link
  understanding completes. Gives hooks access to the fully enriched body
  (transcripts, image descriptions, link summaries) before the agent sees it.

Both hooks are added in get-reply.ts, after applyMediaUnderstanding and
applyLinkUnderstanding. message:received and message:sent are already
in upstream (f07bb8e) and are not duplicated here.

Typed contexts (MessageTranscribedHookContext, MessagePreprocessedHookContext)
and type guards (isMessageTranscribedEvent, isMessagePreprocessedEvent) added
to internal-hooks.ts alongside the existing received/sent types.

Test coverage in src/hooks/message-hooks.test.ts.
@Drickon Drickon force-pushed the feature/message-hooks branch from 744a8c1 to 913c370 Compare February 20, 2026 09:06
@Drickon Drickon changed the title feat(hooks): add message:received, message:transcribed, message:preprocessed, and message:sent internal hooks feat(hooks): add message:transcribed and message:preprocessed internal hooks Feb 20, 2026
Arrow function passed to registerInternalHook was implicitly returning
the number from Array.push(), which is not assignable to void | Promise<void>.
Use block body to discard the return value.
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 size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants