openclaw - 💡(How to fix) Fix Plugin loader silently ignores async register() return value — race conditions on first-traffic [1 comments, 2 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#72947Fetched 2026-04-28 06:29:43
View on GitHub
Comments
1
Participants
2
Timeline
3
Reactions
0
Participants
Timeline (top)
closed ×1commented ×1cross-referenced ×1

In plain English: if a plugin's register() is async, the gateway's plugin loader logs "plugin register returned a promise; async registration is ignored" — and then proceeds as if registration is complete, ignoring whatever async work the plugin is still doing. This creates a small window where hooks are registered but the plugin's services aren't fully initialized; agent invocations in that window have hooks fire but produce no spans / no side effects. The fix is either to await the register promise (the right shape) or document that register() must be synchronous.

Error Message

  • logger.warn("plugin register returned a promise; async registration is ignored ...");
  • Awaiting register: changes timing of plugin load. If a plugin's register hangs, the gateway hangs with it (currently the loader proceeds and the hang is invisible). A timeout-around-await could mitigate, with a clear "plugin X timed out during register" error.

Root Cause

Root cause (best guess)

Fix Action

Fix / Workaround

We hit this with the outshift-open/openclaw-deep-observability plugin, where register() is async (it does await import(...) for OpenClaw plugin SDK lazy loading). The plugin's pattern works around the loader's behavior by deferring real init to a service.start() callback (which DOES get awaited because services are managed differently). But that's a workaround for a sharp edge — other plugin authors will trip on this without realizing.

Workaround fix: keep the warning, change "is ignored" to "is awaited" / "must be sync"

Low-medium. Workaround is straightforward (synchronous register() + async work in service.start()). The user-pain comes from undocumented sharp edges and the "is ignored" warning message that doesn't actually tell you what to do.

Code Example

export default {
  id: "my-plugin",
  async register(api) {       // ← async
    const config = await loadConfig(api);
    api.registerHook("agent_end", () => { /* ... */ });
    api.registerService({
      id: "my-plugin",
      start: async () => { /* fires later, after register returns */ }
    });
  },
};

---

[plugins] plugin register returned a promise; async registration is ignored
(plugin=my-plugin, source=...)

---

-pluginModule.register(api);
-if (returnValue && typeof returnValue.then === "function") {
-  logger.warn("plugin register returned a promise; async registration is ignored ...");
-}
+const returnValue = pluginModule.register(api);
+if (returnValue && typeof returnValue.then === "function") {
+  await returnValue;
+}

---

plugin register returned a promise — register() should be synchronous.
Async work should be deferred to a service.start() callback (registered via
api.registerService(...)). See [docs link]. Hooks registered in this
register() call are active immediately, but any state initialized after
the first `await` may not be ready when hooks first fire.

---

// Test plugin
let registerCompleted = false;
export default {
  id: "test-async-register",
  async register(api) {
    await new Promise(r => setTimeout(r, 1000));
    api.registerHook("after_tool_call", () => {
      console.log("hook fired; registerCompleted =", registerCompleted);
    });
    registerCompleted = true;
  },
};
RAW_BUFFERClick to expand / collapse

Summary

In plain English: if a plugin's register() is async, the gateway's plugin loader logs "plugin register returned a promise; async registration is ignored" — and then proceeds as if registration is complete, ignoring whatever async work the plugin is still doing. This creates a small window where hooks are registered but the plugin's services aren't fully initialized; agent invocations in that window have hooks fire but produce no spans / no side effects. The fix is either to await the register promise (the right shape) or document that register() must be synchronous.

Repro

In a plugin's index.ts:

export default {
  id: "my-plugin",
  async register(api) {       // ← async
    const config = await loadConfig(api);
    api.registerHook("agent_end", () => { /* ... */ });
    api.registerService({
      id: "my-plugin",
      start: async () => { /* fires later, after register returns */ }
    });
  },
};

Gateway log on plugin load:

[plugins] plugin register returned a promise; async registration is ignored
(plugin=my-plugin, source=...)

Hooks ARE registered (the synchronous api.registerHook calls execute before the first await). But anything awaited inside register() continues running concurrently with whatever else the gateway is doing post-register-complete. If a request arrives during that window, hooks fire but the plugin's lazy-resolved state may be null / undefined.

Root cause (best guess)

gateway/dist/<somewhere>/plugin-loader.js calls await pluginModule.register(...) is replaced by pluginModule.register(...) (no await), with a runtime check that the return value is a promise. If it is, log a warning and discard. This is documented in the warning message itself.

(Apologies — we didn't track the exact file:line because we focused on diagnosing the downstream symptom rather than the loader code. Happy to dig in if useful.)

Why it matters

We hit this with the outshift-open/openclaw-deep-observability plugin, where register() is async (it does await import(...) for OpenClaw plugin SDK lazy loading). The plugin's pattern works around the loader's behavior by deferring real init to a service.start() callback (which DOES get awaited because services are managed differently). But that's a workaround for a sharp edge — other plugin authors will trip on this without realizing.

The 9.5-second gap between "hooks registered" and "service.start fires" we observed (filed separately as A15 plugin-double-load) is the visible window where the symptom manifests. During those 9.5 seconds, hook handlers can be invoked but telemetry === null. Empirically we saw this only with the first agent-invocation right after gateway start; in normal operation the gap is hidden because real traffic arrives later.

Suggested fix

Proper fix: await the register promise

-pluginModule.register(api);
-if (returnValue && typeof returnValue.then === "function") {
-  logger.warn("plugin register returned a promise; async registration is ignored ...");
-}
+const returnValue = pluginModule.register(api);
+if (returnValue && typeof returnValue.then === "function") {
+  await returnValue;
+}

This is straightforward IF the surrounding plugin-loader code can be made async. If the call site is synchronous, refactoring the loader to be async-aware is a larger change but the right shape.

Workaround fix: keep the warning, change "is ignored" to "is awaited" / "must be sync"

If async-awareness in the loader is too big a change for this release, at least make the warning message clearer about the implications:

plugin register returned a promise — register() should be synchronous.
Async work should be deferred to a service.start() callback (registered via
api.registerService(...)). See [docs link]. Hooks registered in this
register() call are active immediately, but any state initialized after
the first `await` may not be ready when hooks first fire.

Alternatives considered

  • Refuse to load plugins with async register(): too breaking; existing plugins that work around this correctly (like deep-observability) would be rejected.
  • Await it but with a timeout: hides bugs.

Test plan

// Test plugin
let registerCompleted = false;
export default {
  id: "test-async-register",
  async register(api) {
    await new Promise(r => setTimeout(r, 1000));
    api.registerHook("after_tool_call", () => {
      console.log("hook fired; registerCompleted =", registerCompleted);
    });
    registerCompleted = true;
  },
};

After fix: gateway waits for the 1-second await before declaring the plugin ready. Any hook invocation sees registerCompleted === true. Before fix (current): gateway proceeds immediately. Hooks fired in the first 1-second window see registerCompleted === false.

Risk / blast radius

  • Awaiting register: changes timing of plugin load. If a plugin's register hangs, the gateway hangs with it (currently the loader proceeds and the hang is invisible). A timeout-around-await could mitigate, with a clear "plugin X timed out during register" error.
  • Documentation-only fix: zero risk.

Open questions for maintainers

  1. Is making the plugin loader async feasible in current architecture?
  2. If not, is documenting the synchronous-register contract sufficient, or do you want stricter enforcement (e.g., refuse-to-load on async register)?
  3. Related: A15 (plugin-double-load) suggests there are two separate plugin-loader paths. Is that intentional? If so, both need the async-await treatment.

Tested-against

  • OpenClaw v2026.4.9
  • Plugin: outshift-open/openclaw-deep-observability (main HEAD as of 2026-04-26)

Severity

Low-medium. Workaround is straightforward (synchronous register() + async work in service.start()). The user-pain comes from undocumented sharp edges and the "is ignored" warning message that doesn't actually tell you what to do.

extent analysis

TL;DR

The most likely fix is to await the register promise in the plugin loader to ensure asynchronous registration is properly handled.

Guidance

  • Identify the plugin loader code and modify it to await the register promise, as shown in the suggested fix.
  • If making the plugin loader async is not feasible, update the warning message to clearly indicate that register() should be synchronous and provide guidance on deferring async work to a service.start() callback.
  • Test the fix using a test plugin that simulates an asynchronous registration process.
  • Consider implementing a timeout mechanism to handle cases where a plugin's register hangs.

Example

-pluginModule.register(api);
-if (returnValue && typeof returnValue.then === "function") {
-  logger.warn("plugin register returned a promise; async registration is ignored ...");
-}
+const returnValue = pluginModule.register(api);
+if (returnValue && typeof returnValue.then === "function") {
+  await returnValue;
+}

Notes

The fix assumes that the plugin loader code can be modified to be async-aware. If this is not possible, a documentation-only fix may be sufficient, but it may not provide the same level of reliability as awaiting the register promise.

Recommendation

Apply the workaround fix by updating the warning message to clearly indicate that register() should be synchronous and provide guidance on deferring async work to a service.start() callback. This approach has zero risk and can help mitigate the issue until a more comprehensive fix can be implemented.

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