Skip to content

Comments

feat(hooks): bridge plugin message hooks to internal hooks system#9387

Closed
CyberSinister wants to merge 7 commits intoopenclaw:mainfrom
CyberSinister:feat/message-hooks
Closed

feat(hooks): bridge plugin message hooks to internal hooks system#9387
CyberSinister wants to merge 7 commits intoopenclaw:mainfrom
CyberSinister:feat/message-hooks

Conversation

@CyberSinister
Copy link

@CyberSinister CyberSinister commented Feb 5, 2026

Summary

Implements message:received and message:sent events for the internal hooks system, bridging from the existing plugin hooks.

Closes #8807

Changes

  • Added message to InternalHookEventType
  • Created typed events: MessageReceivedHookEvent, MessageSentHookEvent
  • Added type guards: isMessageReceivedEvent(), isMessageSentEvent()
  • Bridged plugin hooks → internal hooks in dispatch-from-config.ts and deliver.ts
  • Added tests for new hooks
  • Updated docs/hooks.md with new events documentation

Use Cases

  • Memory/RAG integration (auto-recall before response, auto-capture after)
  • Conversation logging and analytics
  • Content filtering (pre/post processing)

Testing

  • 115 tests passing
  • No TypeScript errors
  • Tested error handling (hook failures don't break message flow)

Example Hook

// ~/.openclaw/hooks/message-logger/handler.ts
const handler: HookHandler = async (event) => {
  if (event.type === 'message' && event.action === 'received') {
    console.log(`Message from ${event.context.from}: ${event.context.content}`);
  }
};
export default handler;

Greptile Overview

Greptile Summary

This PR extends the internal HOOK.md-based hook system with a new message event type, adds typed message:received / message:sent event shapes + type guards, and bridges existing plugin message hooks into internal hooks from the inbound dispatch path (src/auto-reply/reply/dispatch-from-config.ts) and outbound delivery path (src/infra/outbound/deliver.ts). Tests were added for the new type guards and handler dispatching, and docs were updated to describe the new message events.

Key integration points:

  • Inbound: after extracting message metadata, it still triggers the plugin message_received hook and additionally emits an internal message:received event.
  • Outbound: after delivering payloads, it triggers the plugin message_sent hook and emits an internal message:sent event.

Issues to address before merge:

  • Outbound hooks currently always emit success: true and only when results.length > 0, which can misreport sends in bestEffort/partial-failure scenarios.
  • Both inbound and outbound internal events can be emitted with an empty sessionKey fallback, making correlation unreliable.

Confidence Score: 3/5

  • This PR is mergeable after fixing a couple of correctness issues in outbound/internal hook event emission.
  • Core changes are localized and covered by unit tests, but the outbound message_sent bridge currently misreports success in partial-failure scenarios and both bridges may emit empty session keys, which can break downstream hook consumers relying on correlation.
  • src/infra/outbound/deliver.ts, src/auto-reply/reply/dispatch-from-config.ts

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

@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, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 378 to 382
// Trigger message_sent hooks (plugin and internal) - refs #8807
if (results.length > 0) {
const lastResult = results.at(-1);
const sentContent = normalizedPayloads.map((p) => p.text ?? "").join("\n");
const hookContext: PluginHookMessageContext = {
Copy link
Contributor

Choose a reason for hiding this comment

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

Incorrect success reporting

message_sent (plugin + internal) is only fired when results.length > 0, and the emitted event hard-codes success: true (deliver.ts:378-416). In bestEffort mode this can be false-positive: a partial/complete failure can still leave results non-empty, so hooks will record a successful send even though one or more payloads failed.

This should either (a) compute success from the per-payload outcomes and include success: false / error when any send failed, or (b) emit one event per payload/result so success/error is accurate for each send attempt.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/deliver.ts
Line: 378:382

Comment:
**Incorrect success reporting**

`message_sent` (plugin + internal) is only fired when `results.length > 0`, and the emitted event hard-codes `success: true` (`deliver.ts:378-416`). In `bestEffort` mode this can be false-positive: a partial/complete failure can still leave `results` non-empty, so hooks will record a successful send even though one or more payloads failed.

This should either (a) compute success from the per-payload outcomes and include `success: false` / `error` when any send failed, or (b) emit one event per payload/result so success/error is accurate for each send attempt.

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

Comment on lines 203 to 207
void triggerInternalHook(
createInternalHookEvent("message", "received", sessionKey ?? "", {
from: ctx.From ?? "",
content,
timestamp,
Copy link
Contributor

Choose a reason for hiding this comment

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

Invalid sessionKey fallback

This bridges inbound messages to internal hooks with sessionKey ?? "" (dispatch-from-config.ts:203-206). The InternalHookEvent.sessionKey is required, so emitting an empty string makes it impossible for hook handlers to reliably correlate events to a session and can cause downstream code to treat unrelated events as the same session.

If there are real cases where sessionKey is unavailable here, it’s better to (1) not fire the internal hook, or (2) populate a meaningful stable key (e.g. derived from channel+conversation) and document that behavior.

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

Comment:
**Invalid sessionKey fallback**

This bridges inbound messages to internal hooks with `sessionKey ?? ""` (`dispatch-from-config.ts:203-206`). The `InternalHookEvent.sessionKey` is required, so emitting an empty string makes it impossible for hook handlers to reliably correlate events to a session and can cause downstream code to treat unrelated events as the same session.

If there are real cases where `sessionKey` is unavailable here, it’s better to (1) not fire the internal hook, or (2) populate a meaningful stable key (e.g. derived from channel+conversation) and document that behavior.

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

Comment on lines 405 to 408
// Bridge to internal hooks (HOOK.md discovery system)
void triggerInternalHook(
createInternalHookEvent("message", "sent", params.mirror?.sessionKey ?? "", {
to,
Copy link
Contributor

Choose a reason for hiding this comment

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

Drops sent hook context

The internal message:sent event uses params.mirror?.sessionKey ?? "" (deliver.ts:405-408). When mirror isn’t provided (common for outbound sends not tied to a session transcript), this emits events with an empty sessionKey, which breaks correlation and makes these events ambiguous.

Either require a session key for firing internal message hooks from this path, or derive a stable correlation key (and document it).

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/infra/outbound/deliver.ts
Line: 405:408

Comment:
**Drops sent hook context**

The internal `message:sent` event uses `params.mirror?.sessionKey ?? ""` (`deliver.ts:405-408`). When `mirror` isn’t provided (common for outbound sends not tied to a session transcript), this emits events with an empty `sessionKey`, which breaks correlation and makes these events ambiguous.

Either require a session key for firing internal message hooks from this path, or derive a stable correlation key (and document it).

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

geoffwhittington pushed a commit to securecheckio/openclaw that referenced this pull request Feb 5, 2026
Enables plugins to block inbound messages by returning { cancel: true }
from the message_received hook, matching message_sending behavior.

Changes:
- Add PluginHookMessageReceivedResult type with content and cancel fields
- Update message_received handler signature to return result or void
- Change runMessageReceived to use runModifyingHook instead of runVoidHook
- Export new result type for plugin consumers

This allows plugins to block malicious or sensitive inbound messages
before they reach the agent.

Builds on PR openclaw#9387 by @CyberSinister
@Zjianru
Copy link

Zjianru commented Feb 7, 2026

Support: Bridging plugin hooks to internal hooks is the right direction / 支持:桥接插件钩子到内部钩子是正确方向

The bridge approach is interesting — unifying plugin-level and internal hooks would give a single event surface for automation.

桥接方案很有意思——统一插件级和内部钩子将为自动化提供单一的事件接口。

I've been running a multi-agent setup and built an external events-framework to work around the lack of native event hooks. The core need: when something happens (message received, sub-agent completed, error occurred), notify the user immediately with rate-limiting and deduplication — without burning tokens on cron polling.

我运行着一个多 Agent 系统,并构建了一个外部 events-framework 来绕过缺少原生事件钩子的问题。核心需求:当事情发生时(收到消息、子 Agent 完成、发生错误),立即通知用户,带限频和去重——不用 cron 轮询烧 token。

There are now 3 PRs (#7545, #9387, #9859) tackling the same gap. Would be great to get maintainer guidance on the preferred direction.

现在有 3 个 PR(#7545#9387#9859)在解决同一个缺口。希望能得到维护者对首选方向的指导。

Samantha added 2 commits February 9, 2026 12:50
Implements bridging between the plugin hook system and the internal
HOOK.md discovery-based hook system for message events.

Changes:
- Add 'message' to InternalHookEventType
- Add MessageReceivedHookEvent and MessageSentHookEvent types
- Add isMessageReceivedEvent and isMessageSentEvent type guards
- Trigger internal hooks from dispatch-from-config when messages received
- Trigger internal hooks from deliver.ts when messages sent
- Add comprehensive tests for new message hook types
- Update docs/hooks.md with new Message Events section
- Remove message events from Future Events section

This allows HOOK.md-based hooks to listen for message:received and
message:sent events, enabling automation based on message flow without
needing to implement plugin hooks.

Closes openclaw#8807
- Fix outbound hooks to report accurate success status (was always true)
- Track partial failures in bestEffort mode with deliveredCount/attemptedCount
- Skip internal hook emission when sessionKey unavailable (was emitting empty string)
- Wrap message:received hook in sessionKey check for reliable correlation
Samantha added 2 commits February 11, 2026 19:13
- Add GatewayStuckDetectionConfig to config types
- Extend ChatRunEntry with startedAt timestamp for tracking
- Add entries() and size() methods to ChatRunRegistry
- Add stuck detection sweep in maintenance timer (60s interval)
- Configurable threshold (default 5 min) and action (log/notify/abort)
- Wire up in server.impl.ts with stuckDetection config

Closes: session reliability improvements
@TekSiDoT
Copy link

Looking forward to seing this merged, will enable a 'after-the-fact'-check on agent messages.

Will enable https://beercan-with-a-soul.dev/dev/commitment-nagger/

@CyberSinister
Copy link
Author

Friendly bump — this PR has been open since Feb 5 with all CI checks passing ✅ and Greptile review feedback addressed. There are now 3 PRs (#2307, #9387, #14196) tackling the same message hooks gap. Would love maintainer guidance on the preferred direction so we can consolidate efforts. Happy to make any changes needed to align with the project's vision. 🙏

…ction

- Fix stuck run cleanup for voice-triggered runs by calling removeChatRun
  with sessionId (registry key) before abortChatRunById
- Differentiate 'notify' action from 'log' by broadcasting stuck-run event
  to connected clients and subscribed nodes
Samantha added 2 commits February 17, 2026 12:00
… detection

- server.impl.ts: logGateway is not in scope, use local 'log' variable
- server-maintenance.ts: snapshot registry entries before iterating to
  prevent skipped entries when abort mutates the underlying Map/arrays

Addresses Greptile review round 3 feedback on openclaw#16125
@steipete
Copy link
Contributor

Backported core message hook bridge to main in 01cc1feca (fix(hooks): backport internal message hook bridge with safe delivery semantics).

Included backport fixes:

  • per-payload message_sent success/error emission (chunk-safe)
  • internal hook session-key guards
  • targeted regression tests + changelog entries

Skipped noisy/unrelated changes (including lockfile and gateway stuck-detection feature).

@steipete steipete closed this Feb 17, 2026
@steipete
Copy link
Contributor

Final landed commit on main after rebase: f07bb8e.\nIncludes changelog/docs/tests + scoped backport of the core hook bridge.

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 gateway Gateway runtime size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Implement message:received and message:sent hooks

4 participants