openclaw - 💡(How to fix) Fix Allow plugins to take over dispatch via before_dispatch without core marking inbound as completed

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

I'm building a plugin (inbound-persistence) that persists every inbound WhatsApp message into a SQLite queue with per-sender FIFO semantics. The goal: serialize dispatch per peer so that 3 messages arriving in a 5-second window from the same sender are processed one at a time and the agent's responses arrive in the same order the user typed them.

Root Cause

Without it, plugins cannot implement:

  • FIFO-per-sender message ordering
  • Rate-limited agent calls per peer
  • Priority queues across channels
  • Any "deferred dispatch" pattern

Currently the only workarounds are (a) forking openclaw, (b) patching dist/*.js in place (lost on npm update), or (c) routing through an external broker that bypasses openclaw's channels entirely — all of which are significantly worse than the proposed flag.

Fix Action

Fix / Workaround

I'm building a plugin (inbound-persistence) that persists every inbound WhatsApp message into a SQLite queue with per-sender FIFO semantics. The goal: serialize dispatch per peer so that 3 messages arriving in a 5-second window from the same sender are processed one at a time and the agent's responses arrive in the same order the user typed them.

The before_dispatch hook is exposed (see hook-types.d.ts), but if a plugin returns { handled: true } from it, the core treats the inbound as fully completed:

// dist/dispatch-BGwFKOyt.js around line 985-988
markIdle("plugin_binding_dispatch");
recordProcessed("completed", { reason: "plugin-bound-handled" });
commitInboundDedupeIfClaimed();
return attachSourceReplyDeliveryMode({...});

Code Example

// dist/dispatch-BGwFKOyt.js around line 985-988
markIdle("plugin_binding_dispatch");
recordProcessed("completed", { reason: "plugin-bound-handled" });
commitInboundDedupeIfClaimed();
return attachSourceReplyDeliveryMode({...});

---

type BeforeDispatchResult = {
  handled?: boolean;
  takeover?: boolean;   // NEW
};
RAW_BUFFERClick to expand / collapse

Affected version: openclaw 2026.5.27 (observed since 2026.5.18)

Context

I'm building a plugin (inbound-persistence) that persists every inbound WhatsApp message into a SQLite queue with per-sender FIFO semantics. The goal: serialize dispatch per peer so that 3 messages arriving in a 5-second window from the same sender are processed one at a time and the agent's responses arrive in the same order the user typed them.

Problem

The before_dispatch hook is exposed (see hook-types.d.ts), but if a plugin returns { handled: true } from it, the core treats the inbound as fully completed:

// dist/dispatch-BGwFKOyt.js around line 985-988
markIdle("plugin_binding_dispatch");
recordProcessed("completed", { reason: "plugin-bound-handled" });
commitInboundDedupeIfClaimed();
return attachSourceReplyDeliveryMode({...});

After this, any attempt by the plugin to re-dispatch the same msgId is silently swallowed by the inbound-dedupe layer. There is no way to say "I'm taking ownership, defer this, I'll re-inject it through the runtime later" without losing the message.

Expected behavior

A plugin should be able to defer dispatch — keep the message claimable, not commit dedupe — so that the plugin can later call the runtime's inject API on its own schedule.

Proposed solution (minimally invasive)

Extend the before_dispatch return contract with a new flag:

type BeforeDispatchResult = {
  handled?: boolean;
  takeover?: boolean;   // NEW
};

When { handled: true, takeover: true } is returned:

  • Skip recordProcessed("completed", ...)
  • Skip commitInboundDedupeIfClaimed()
  • Keep the inbound in claimed state
  • Expose a runtime API the plugin can call when ready: runtime.releaseTakeover(msgId, { redispatch: true })

The current behavior ({ handled: true } alone) stays unchanged for backwards compatibility.

Why this matters

Without it, plugins cannot implement:

  • FIFO-per-sender message ordering
  • Rate-limited agent calls per peer
  • Priority queues across channels
  • Any "deferred dispatch" pattern

Currently the only workarounds are (a) forking openclaw, (b) patching dist/*.js in place (lost on npm update), or (c) routing through an external broker that bypasses openclaw's channels entirely — all of which are significantly worse than the proposed flag.

Repro

  1. Install openclaw 2026.5.27
  2. Write a plugin with a before_dispatch handler that returns { handled: true }
  3. From the same handler, try to call any runtime dispatch API to re-inject the same message a few seconds later
  4. Observe: the re-dispatch is silently dropped by the inbound-dedupe layer

I have a complete failed attempt (plugin-side workaround code + post-mortem notes from 2026-05-28) I can share if useful — happy to provide it on request.

Environment

  • openclaw 2026.5.27
  • Ubuntu 24.04, node 22.22.2
  • Use case: WhatsApp channel via Baileys, ~300+ inbounds/week, mix of groups + DMs

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

A plugin should be able to defer dispatch — keep the message claimable, not commit dedupe — so that the plugin can later call the runtime's inject API on its own schedule.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - 💡(How to fix) Fix Allow plugins to take over dispatch via before_dispatch without core marking inbound as completed