Skip to content

feat(plugins): add cancel support to message_received hook#11681

Open
PrimeTenet wants to merge 3 commits intoopenclaw:mainfrom
securecheckio:feat/plugin-message-interception
Open

feat(plugins): add cancel support to message_received hook#11681
PrimeTenet wants to merge 3 commits intoopenclaw:mainfrom
securecheckio:feat/plugin-message-interception

Conversation

@PrimeTenet
Copy link

@PrimeTenet PrimeTenet commented Feb 8, 2026

Summary

Enables plugins to block inbound messages by returning { cancel: true } from the message_received hook, matching the existing message_sending cancel behavior.

Changes

3 files changed:

  • src/plugins/types.ts - Add PluginHookMessageReceivedResult type with cancel field
  • src/plugins/hooks.ts - Collect results from parallel hook execution, return { cancel: true } if any hook cancelled
  • src/auto-reply/reply/dispatch-from-config.ts - Await hook results and stop processing if message marked for cancel

Usage

api.on('message_received', async (event, ctx) => {
if (containsThreat(event.content)) {
return { cancel: true }; // Block the message
}
// Allow through
}, { priority: 100 });

Use Cases

  • Security/DLP plugins blocking malicious content
  • Content moderation (spam, abuse)
  • Rate limiting excessive messages
  • Access control

Behavior

  • Hooks run in parallel (preserves existing Promise.all behavior)
  • If any hook returns { cancel: true }, message is blocked
  • Dispatcher now awaits hook results to check cancel flag (was fire-and-forget)

Breaking Changes

Latency impact: Messages now wait for all message_received hooks to complete before processing (was fire-and-forget). Adds ~10ms latency per message if hooks are present.

Plugins should be optimized for fast execution (<10ms recommended).

Backward Compatibility

Existing message_received hooks that return void continue to work unchanged.

Greptile Overview

Greptile Summary

Adds a cancellable return type for the message_received plugin hook ({ cancel?: boolean }), updates the hook runner to execute all message_received handlers in parallel and aggregate their results, and changes the config-based dispatcher to await message_received hooks and stop further processing when any hook cancels.

This fits into the existing plugin hook system by extending the hook type map in src/plugins/types.ts and exposing the new result type via src/plugins/hooks.ts, then consuming the cancel result in the inbound message dispatch path.

Confidence Score: 3/5

  • This PR is close to mergeable but has a cancel-path lifecycle/diagnostics bug that should be fixed first.
  • Core typing and hook aggregation look consistent, but early-return on cancel bypasses existing message processing/idle tracking in the dispatcher, which can break diagnostics/session state accounting. There’s also a behavior change (message_received now blocks) that should be made consistent with expectations across the codebase.
  • src/auto-reply/reply/dispatch-from-config.ts

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Enables plugins to block inbound messages by returning { cancel: true },
matching existing message_sending cancel behavior.

Changes:
- Add PluginHookMessageReceivedResult type with cancel field
- Update runMessageReceived to collect results from parallel hook execution
- Process cancel in dispatch-from-config.ts to stop message processing

Hook execution: Preserves existing parallel execution (Promise.all).
Cancel merge: If ANY hook returns { cancel: true }, message is cancelled.

Use cases:
- Security plugins blocking malicious content
- Content moderation
- Rate limiting
- Access control

Breaking changes: None (purely additive)

Co-authored-by: Cursor <cursoragent@cursor.com>
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 +255 to +258
// Run all hooks in parallel (preserves existing behavior)
const promises = hooks.map(async (hook) => {
try {
return await (
Copy link
Contributor

Choose a reason for hiding this comment

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

message_received no longer fire-and-forget

runMessageReceived now awaits Promise.all(promises) and returns a result, which changes runtime behavior (it blocks the caller until all hooks finish). The docstring still implies the previous behavior via “preserves existing behavior”/parallel wording; if other inbound paths rely on fire-and-forget semantics this change will introduce head-of-line blocking. Either keep message_received fire-and-forget and introduce a separate cancellable variant, or update callers/expectations consistently.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/hooks.ts
Line: 255:258

Comment:
**`message_received` no longer fire-and-forget**

`runMessageReceived` now `await`s `Promise.all(promises)` and returns a result, which changes runtime behavior (it blocks the caller until all hooks finish). The docstring still implies the previous behavior via “preserves existing behavior”/parallel wording; if other inbound paths rely on fire-and-forget semantics this change will introduce head-of-line blocking. Either keep `message_received` fire-and-forget and introduce a separate cancellable variant, or update callers/expectations consistently.


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

Comment on lines +169 to 172
try {
const hookResult = await hookRunner.runMessageReceived(
{
from: ctx.From ?? "",
Copy link
Contributor

Choose a reason for hiding this comment

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

Cancel path skips diagnostics

When hookResult?.cancel is true, the function returns early (dispatch is dropped) but doesn’t call recordProcessed(...)/markIdle(...). With diagnostics enabled and a session key present, this can leave the session in a “processing” state without a corresponding “idle” transition and no processed outcome recorded.

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

Comment:
**Cancel path skips diagnostics**

When `hookResult?.cancel` is true, the function `return`s early (dispatch is dropped) but doesn’t call `recordProcessed(...)`/`markIdle(...)`. With diagnostics enabled and a session key present, this can leave the session in a “processing” state without a corresponding “idle” transition and no processed outcome recorded.


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

PrimeTenet and others added 2 commits February 8, 2026 01:35
- Update docstring to clarify message_received now blocks until all hooks complete
- Add diagnostics cleanup (recordProcessed/markIdle) when message is cancelled
  to prevent session tracking from getting stuck in processing state

Addresses reviewer feedback on PR openclaw#11681

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Feb 21, 2026
@openclaw-barnacle
Copy link

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant