Skip to content

dispatch: broadcast inbound_claim to global plugin listeners#58862

Open
flowolforg wants to merge 1 commit intoopenclaw:mainfrom
flowolforg:patch-3
Open

dispatch: broadcast inbound_claim to global plugin listeners#58862
flowolforg wants to merge 1 commit intoopenclaw:mainfrom
flowolforg:patch-3

Conversation

@flowolforg
Copy link
Copy Markdown

Problem

inbound_claim currently only fires for plugin-bound conversations (via runInboundClaimForPluginOutcome). Plugins that register global handlers — e.g. listen-only observers, spam filters, rate limiters — are never invoked for unbound inbound events.

Reported in #48434.

Change

After the if (pluginOwnedBinding) block in dispatch-from-config.ts, add a single broadcast call to hookRunner.runInboundClaim(). This iterates all registered global listeners regardless of conversation binding. If any listener returns { handled: true }, dispatch stops early (same early-return pattern used for plugin-bound handling).

+  // Broadcast inbound_claim to global plugin listeners (fixes #48434).
+  if (hookRunner) {
+    const broadcastResult = await hookRunner.runInboundClaim(
+      inboundClaimEvent,
+      inboundClaimContext,
+    );
+    if (broadcastResult?.handled) {
+      markIdle("plugin_broadcast_claim");
+      recordProcessed("completed", { reason: "plugin-broadcast-handled" });
+      return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
+    }
+  }

Why this is safe

  • All supporting infrastructure (runInboundClaim, types, runners, event mappers, tests) already exists from PR Plugins: broaden plugin surface for Codex App Server #45318 — this is purely a missing call-site.
  • Backward-compatible: plugins that don't register a global handler are unaffected. The existing plugin-bound flow runs first and is unchanged.
  • No new dependencies.

Testing

Existing tests in dispatch-from-config.test.ts and wired-hooks-inbound-claim.test.ts cover the runner. I've validated the broadcast path locally against the openclaw-listen-only plugin (https://github.com/flowolforg/openclaw-listen-only) on a Telegram group chat.

Closes #48434

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR fixes a gap in plugin hook dispatch: inbound_claim was only fired for conversations already bound to a specific plugin, meaning global listen-only observers, spam filters, and rate limiters never received unbound inbound events. The fix adds a single hookRunner.runInboundClaim() broadcast call immediately after the existing plugin-bound block, matching the first-claim-wins pattern already used for before_dispatch.

The implementation is correct and the logic is safe:

  • No double-invocation risk: runInboundClaimForPluginOutcome only falls through to the broadcast in the missing_plugin and no_handler cases, both of which mean the bound plugin has no registered inbound_claim handlers and therefore won't be called in the global broadcast either.
  • Existing bound-claim flow is unchanged: handled, declined, and error outcomes all return early before the broadcast is reached.
  • Backward compatible: Deployments without global inbound_claim handlers are unaffected — runClaimingHook returns undefined immediately when no hooks are registered.
  • One minor style inconsistency: the new call omits the hasHooks(\"inbound_claim\") guard used by the adjacent before_dispatch and message_received calls; this is functionally harmless but diverges from the established pattern and adds an unnecessary async call + registry lookup on every message in the no-plugin case.

Confidence Score: 5/5

Safe to merge — the change is a correct, targeted call-site addition with no risk of double-invocation or regression on existing plugin-bound flows.

All remaining findings are P2 (a minor style inconsistency around the missing hasHooks guard). The core logic is sound: early-return paths for handled, declined, and error outcomes are unaffected; the broadcast only fires in the missing_plugin/no_handler fallback cases where no plugin has claimed the message; and runClaimingHook handles empty hook lists gracefully. No data integrity, security, or reliability concerns.

No files require special attention.

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

Comment:
**Missing `hasHooks` guard — minor inconsistency with adjacent patterns**

Every other claiming/observing hook call in this function guards on `hasHooks(...)` before calling the runner method (see `before_dispatch` at line 565 and `message_received` at line 434). The new broadcast call skips that guard.

`runClaimingHook` does return `undefined` immediately when no hooks are registered, so there is no functional difference. But the unconditional call still pays for an async dispatch and a registry lookup on every inbound message, even when no plugin has ever registered an `inbound_claim` handler (the common case for most deployments).

```suggestion
  if (hookRunner?.hasHooks("inbound_claim")) {
    const broadcastResult = await hookRunner.runInboundClaim(
      inboundClaimEvent,
      inboundClaimContext,
    );
    if (broadcastResult?.handled) {
      markIdle("plugin_broadcast_claim");
      recordProcessed("completed", { reason: "plugin-broadcast-handled" });
      return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
    }
  }
```

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

Reviews (1): Last reviewed commit: "dispatch: broadcast inbound_claim to glo..." | Re-trigger Greptile

Comment on lines +421 to +431
if (hookRunner) {
const broadcastResult = await hookRunner.runInboundClaim(
inboundClaimEvent,
inboundClaimContext,
);
if (broadcastResult?.handled) {
markIdle("plugin_broadcast_claim");
recordProcessed("completed", { reason: "plugin-broadcast-handled" });
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Missing hasHooks guard — minor inconsistency with adjacent patterns

Every other claiming/observing hook call in this function guards on hasHooks(...) before calling the runner method (see before_dispatch at line 565 and message_received at line 434). The new broadcast call skips that guard.

runClaimingHook does return undefined immediately when no hooks are registered, so there is no functional difference. But the unconditional call still pays for an async dispatch and a registry lookup on every inbound message, even when no plugin has ever registered an inbound_claim handler (the common case for most deployments).

Suggested change
if (hookRunner) {
const broadcastResult = await hookRunner.runInboundClaim(
inboundClaimEvent,
inboundClaimContext,
);
if (broadcastResult?.handled) {
markIdle("plugin_broadcast_claim");
recordProcessed("completed", { reason: "plugin-broadcast-handled" });
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
}
}
if (hookRunner?.hasHooks("inbound_claim")) {
const broadcastResult = await hookRunner.runInboundClaim(
inboundClaimEvent,
inboundClaimContext,
);
if (broadcastResult?.handled) {
markIdle("plugin_broadcast_claim");
recordProcessed("completed", { reason: "plugin-broadcast-handled" });
return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/dispatch-from-config.ts
Line: 421-431

Comment:
**Missing `hasHooks` guard — minor inconsistency with adjacent patterns**

Every other claiming/observing hook call in this function guards on `hasHooks(...)` before calling the runner method (see `before_dispatch` at line 565 and `message_received` at line 434). The new broadcast call skips that guard.

`runClaimingHook` does return `undefined` immediately when no hooks are registered, so there is no functional difference. But the unconditional call still pays for an async dispatch and a registry lookup on every inbound message, even when no plugin has ever registered an `inbound_claim` handler (the common case for most deployments).

```suggestion
  if (hookRunner?.hasHooks("inbound_claim")) {
    const broadcastResult = await hookRunner.runInboundClaim(
      inboundClaimEvent,
      inboundClaimContext,
    );
    if (broadcastResult?.handled) {
      markIdle("plugin_broadcast_claim");
      recordProcessed("completed", { reason: "plugin-broadcast-handled" });
      return { queuedFinal: false, counts: dispatcher.getQueuedCounts() };
    }
  }
```

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

@SonicBotMan

This comment was marked as spam.

Add a broadcast call to hookRunner.runInboundClaim() after the
plugin-bound block in dispatch-from-config.ts. This lets global
plugin handlers (spam filters, rate limiters, group-chat observers
like listen-only) receive all unbound inbound events.

- Uses existing infrastructure from PR openclaw#45318
- hasHooks("inbound_claim") guard matches adjacent hook patterns
- Early-return when any listener returns { handled: true }

Closes openclaw#48434
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Broadcast inbound_claim hook to all plugins (not just plugin-bound conversations)

2 participants