feat(hooks): bridge plugin message hooks to internal hooks system#9387
feat(hooks): bridge plugin message hooks to internal hooks system#9387CyberSinister wants to merge 7 commits intoopenclaw:mainfrom
Conversation
| // 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 = { |
There was a problem hiding this 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.
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.| void triggerInternalHook( | ||
| createInternalHookEvent("message", "received", sessionKey ?? "", { | ||
| from: ctx.From ?? "", | ||
| content, | ||
| timestamp, |
There was a problem hiding this 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.
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.
src/infra/outbound/deliver.ts
Outdated
| // Bridge to internal hooks (HOOK.md discovery system) | ||
| void triggerInternalHook( | ||
| createInternalHookEvent("message", "sent", params.mirror?.sessionKey ?? "", { | ||
| to, |
There was a problem hiding this 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).
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.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
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. |
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
- 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
|
Looking forward to seing this merged, will enable a 'after-the-fact'-check on agent messages. |
|
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
ebf300c to
1b6f104
Compare
bfc1ccb to
f92900f
Compare
… 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
|
Backported core message hook bridge to Included backport fixes:
Skipped noisy/unrelated changes (including lockfile and gateway stuck-detection feature). |
|
Final landed commit on main after rebase: f07bb8e.\nIncludes changelog/docs/tests + scoped backport of the core hook bridge. |
Summary
Implements
message:receivedandmessage:sentevents for the internal hooks system, bridging from the existing plugin hooks.Closes #8807
Changes
messagetoInternalHookEventTypeMessageReceivedHookEvent,MessageSentHookEventisMessageReceivedEvent(),isMessageSentEvent()dispatch-from-config.tsanddeliver.tsdocs/hooks.mdwith new events documentationUse Cases
Testing
Example Hook
Greptile Overview
Greptile Summary
This PR extends the internal HOOK.md-based hook system with a new
messageevent type, adds typedmessage:received/message:sentevent 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:
message_receivedhook and additionally emits an internalmessage:receivedevent.message_senthook and emits an internalmessage:sentevent.Issues to address before merge:
success: trueand only whenresults.length > 0, which can misreport sends inbestEffort/partial-failure scenarios.sessionKeyfallback, making correlation unreliable.Confidence Score: 3/5
message_sentbridge currently misreports success in partial-failure scenarios and both bridges may emit empty session keys, which can break downstream hook consumers relying on correlation.(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!