openclaw - 💡(How to fix) Fix Plugin loader silently truncates async register() — 'returned a promise; async registration is ignored' [1 comments, 1 participants]

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…
GitHub stats
openclaw/openclaw#72941Fetched 2026-04-28 06:29:53
View on GitHub
Comments
1
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
cross-referenced ×2closed ×1commented ×1

Error Message

log.warn(plugin register returned a promise; async registration is ignored ...); Option B — keep the loader synchronous, document the contract clearly, and elevate the warning to an error in the next major version.

Root Cause

The plugin loader's call site for register() is synchronous:

// pseudo-code from gateway plugin loader
const result = plugin.register(api);
if (isPromise(result)) {
  log.warn(`plugin register returned a promise; async registration is ignored ...`);
  // Note: result is discarded, gateway moves on.
}

The intent (per the warning text) appears to be that plugins should make register() synchronous and defer async work to a service start() callback. But that contract isn't documented in user-facing docs, only in the warning log line.

Fix Action

Fix / Workaround

In plain English: the gateway's plugin loader calls a plugin's register() synchronously and ignores its returned promise — so any setup the plugin does after the first await runs after registration is reported complete. Hooks that fire during the gap see a half-initialized plugin. The gateway log even says "async registration is ignored", but it's a warning that's easy to miss and the workaround (synchronous register, async work in start()) is undocumented.

Code Example

[plugins] plugin register returned a promise; async registration is ignored
  (plugin=openclaw-deep-observability,
   source=/sandbox/.openclaw-data/extensions/openclaw-deep-observability/index.ts)

---

// some-plugin/index.ts
export async function register(api) {
  api.registerHook('message_received', someHandler);  // synchronous, fires immediately
  await someAsyncSetupThatTakesAWhile();               // truncated by the loader
  api.registerHook('agent_end', anotherHandler);       // may or may not be registered before first traffic
}

---

// pseudo-code from gateway plugin loader
const result = plugin.register(api);
if (isPromise(result)) {
  log.warn(`plugin register returned a promise; async registration is ignored ...`);
  // Note: result is discarded, gateway moves on.
}

---

export function register(api) {            // sync, just registers hooks
  api.registerHook('message_received', someHandler);
}

export async function init(api) {           // async, gets awaited by loader before traffic
  await someAsyncSetupThatTakesAWhile();
}
RAW_BUFFERClick to expand / collapse

In plain English: the gateway's plugin loader calls a plugin's register() synchronously and ignores its returned promise — so any setup the plugin does after the first await runs after registration is reported complete. Hooks that fire during the gap see a half-initialized plugin. The gateway log even says "async registration is ignored", but it's a warning that's easy to miss and the workaround (synchronous register, async work in start()) is undocumented.

Problem

When a plugin's register() returns a promise (i.e., is async or returns a deferred result), the gateway's plugin loader emits a warning and proceeds without awaiting:

[plugins] plugin register returned a promise; async registration is ignored
  (plugin=openclaw-deep-observability,
   source=/sandbox/.openclaw-data/extensions/openclaw-deep-observability/index.ts)

This is from outshift-open/openclaw-deep-observability's index.ts, which is async function register(api) — the plugin's setup includes async work that the loader silently truncates.

Reproducer

// some-plugin/index.ts
export async function register(api) {
  api.registerHook('message_received', someHandler);  // synchronous, fires immediately
  await someAsyncSetupThatTakesAWhile();               // truncated by the loader
  api.registerHook('agent_end', anotherHandler);       // may or may not be registered before first traffic
}

When someAsyncSetupThatTakesAWhile() takes >0ms, the loader returns to the gateway boot path before the second registerHook fires. Any agent invocation in that window sees only the first hook handler. Race conditions get hidden by timing — under load, the bug is intermittent.

Root cause

The plugin loader's call site for register() is synchronous:

// pseudo-code from gateway plugin loader
const result = plugin.register(api);
if (isPromise(result)) {
  log.warn(`plugin register returned a promise; async registration is ignored ...`);
  // Note: result is discarded, gateway moves on.
}

The intent (per the warning text) appears to be that plugins should make register() synchronous and defer async work to a service start() callback. But that contract isn't documented in user-facing docs, only in the warning log line.

Proposed fix

Option A — await the register promise (proper fix).

Change the plugin loader to await the result of register() if it's a promise. This requires the loader's caller to be async or use .then(). Backwards-compatible — synchronous register callers see no change.

Option B — keep the loader synchronous, document the contract clearly, and elevate the warning to an error in the next major version.

Less code change, but kicks the issue down the road and depends on every plugin author seeing the warning.

Option C — add an explicit init() async hook to the plugin API, separate from register().

export function register(api) {            // sync, just registers hooks
  api.registerHook('message_received', someHandler);
}

export async function init(api) {           // async, gets awaited by loader before traffic
  await someAsyncSetupThatTakesAWhile();
}

Cleanest API, but an addition rather than a fix — existing plugins still need migrating.

I'd recommend Option A as the immediate fix (no API change required, every plugin gets the right behavior automatically) and Option C as a long-term shape (clearer contract).

Observed downstream effect

In the deep-observability plugin's case, register() returns a promise (it imports ./hooks.js and ./telemetry.js and registers handlers asynchronously). The loader truncates the registration. Then the gateway logs [plugins] plugin register returned a promise; async registration is ignored, and ~7s later the embedded acpx runtime backend registers the plugin a SECOND time (separate friction note, sibling issue) — at which point its service.start() callback fires and OTel SDK initialization happens. Until that second load, hooks may produce no spans (or partial spans). This compounds with another issue (#38 trace context not propagated for sub-agents) — both have race-condition flavors that are hard to repro under load.

Alternatives considered

  • Document and let plugin authors handle it. Already what's happening; doesn't scale. Each new plugin author hits the same gotcha.
  • Make the plugin API force-synchronous. Doesn't reflect the reality that meaningful plugin setup often involves dynamic imports, file reads, network probes, etc.

Test plan

  • Unit: assert that an async register() is awaited by the loader before any hook handler fires.
  • Integration: load a test plugin whose register() does a 100ms await and registers a message_received hook AFTER the await. Assert that an immediate test message receives the hook.
  • Regression: synchronous register() plugins should continue to work unchanged.

Risk / blast radius

  • Backwards-compatible at the plugin author level — sync register() callers see no behavior change.
  • Introduces a small additional startup latency for plugins with async register (a few ms to seconds depending on what they do). Acceptable: that's correctness work the plugin asked the loader to do.
  • The "register returned a promise" warning would no longer fire — could be a confusing signal for anyone looking for it. Replace with [plugins] awaited async register for <plugin> (took Xms) so the diagnostic value is preserved.

Open questions

  1. Is the loader-is-sync constraint deliberate (e.g., to prevent slow plugins from stalling gateway boot)? If so, a separate timeout knob (OPENCLAW_PLUGIN_REGISTER_TIMEOUT_MS) might be the right shape, with synchronous fallback on timeout.
  2. Preferred between Option A (await) and Option C (separate init() hook)? My read: A is the right immediate fix; C is the right long-term API.
  3. Do any existing plugins rely on the truncated-registration behavior (i.e., expect hook registration to happen "later" without being awaited)? Unlikely but worth a grep.

Tested-against: OpenClaw v2026.4.9 inside a NemoClaw v0.0.26 sandbox running deep-observability main HEAD as of 2026-04-26. Sibling findings: #72938 (plugins install path), #72939 (mDNS networkInterfaces crash), and forthcoming filings for openclaw gateway stop no-op + double plugin-load timing.

extent analysis

TL;DR

The most likely fix is to modify the plugin loader to await the result of register() if it's a promise, ensuring asynchronous plugin setup is completed before proceeding.

Guidance

  • Identify plugins with asynchronous register() functions and verify if they are affected by the truncated registration issue.
  • Consider implementing Option A, awaiting the register() promise, as the immediate fix to ensure correct plugin initialization.
  • Evaluate the need for a separate init() hook (Option C) as a long-term solution to clarify the plugin API contract.
  • Review existing plugins for potential reliance on the current truncated-registration behavior to assess the risk of changing the loader's behavior.

Example

// Modified plugin loader pseudo-code
const result = plugin.register(api);
if (isPromise(result)) {
  await result; // Await the promise to ensure async setup is completed
}

Notes

The choice between Option A and Option C depends on the specific requirements and constraints of the plugin ecosystem. Option A provides a more immediate fix, while Option C offers a cleaner API but requires plugin updates.

Recommendation

Apply Option A (await the register() promise) as the immediate fix, as it ensures correct plugin initialization without requiring API changes. This approach is backwards-compatible and introduces minimal additional startup latency.

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…

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 Plugin loader silently truncates async register() — 'returned a promise; async registration is ignored' [1 comments, 1 participants]