openclaw - ✅(Solved) Fix fix(plugins): infinite recursion in facade module loader crashes xai, sglang, vllm [2 pull requests, 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#57394Fetched 2026-04-08 01:50:08
View on GitHub
Comments
1
Participants
2
Timeline
15
Reactions
0
Participants
Timeline (top)
referenced ×8cross-referenced ×4closed ×1commented ×1

After 8e0ab35b0e ("refactor(plugins): decouple bundled plugin runtime loading"), the xai, sglang, and vllm plugins crash on every load attempt with RangeError: Maximum call stack size exceeded. The gateway retries loading these plugins every ~8 minutes, each time hitting the same stack overflow.

Error Message

[plugins] xai failed to load from extensions/xai/index.ts: RangeError: Maximum call stack size exceeded [plugins] sglang failed to load from extensions/sglang/index.ts: RangeError: Maximum call stack size exceeded [plugins] vllm failed to load from extensions/vllm/index.ts: RangeError: Maximum call stack size exceeded

Root Cause

The facade lazy-loading pattern introduced in 8e0ab35b0e handles function exports correctly (wrapped in closures, loaded on first call) but constant exports eagerly call loadFacadeModule() at module-evaluation time:

// Functions — lazy, OK:
export const buildXaiProvider: F["buildXaiProvider"] = ((...args) =>
  loadFacadeModule()["buildXaiProvider"](...args)) as F["buildXaiProvider"];

// Constants — eager, crashes:
export const XAI_BASE_URL: F["XAI_BASE_URL"] = loadFacadeModule()["XAI_BASE_URL"];

When the constant initializer triggers loadFacadeModule() → Jiti sync load of extensions/xai/api.ts, any transitive dependency that imports back to openclaw/plugin-sdk/xai (even indirectly) re-enters loadBundledPluginPublicSurfaceModuleSync(). The loadedFacadeModules cache is only set after the Jiti load returns (line 204 of facade-runtime.ts), so the re-entrant call finds no cache entry and recurses until stack overflow.

The xai, sglang, and vllm extensions are affected because their api.ts files export constants and their dependency graphs have circular paths back through the SDK facades.

Fix Action

Fix / Workaround

  1. Upgrade to any commit after 8e0ab35b0e
  2. Run pnpm build
  3. Start the gateway: node dist/index.js gateway --port 18789
  4. Observe stderr — xai, sglang, and vllm fail in a retry loop:

Patch branch

  1. Stale dist/ after git pull — the compiled plugin SDK .d.ts files were missing exports for createSubsystemLogger, registerCliBackend, and createResolvedDirectoryEntriesLister, which broke amazon-bedrock, anthropic, google, and matrix plugins. Fixed by running pnpm build.

PR fix notes

PR #57435: fix(facade-runtime): add recursion guard to facade module loader to prevent infinite stack overflow

Description (problem / solution / changelog)

Summary

  • Problem: The xai, sglang, and vllm plugins crash on every load attempt with RangeError: Maximum call stack size exceeded. The gateway retries loading these plugins every ~8 minutes, each time hitting the same stack overflow. This occurs in src/plugin-sdk/facade-runtime.ts at loadBundledPluginPublicSurfaceModuleSync() (line 171-206).
  • Root Cause: The facade lazy-loading pattern introduced in 8e0ab35b0e handles function exports correctly (wrapped in closures, loaded on first call) but constant exports eagerly call loadFacadeModule() at module-evaluation time. When the constant initializer triggers loadFacadeModule() → Jiti sync load of extensions/{xai,sglang,vllm}/api.ts, any transitive dependency that imports back to openclaw/plugin-sdk/{xai,sglang,vllm} (even indirectly) re-enters loadBundledPluginPublicSurfaceModuleSync(). The loadedFacadeModules cache is only set after the Jiti load returns (line 204), so the re-entrant call finds no cache entry and recurses until stack overflow.
  • Fix: Set a sentinel object in the loadedFacadeModules cache before the Jiti load begins. Re-entrant calls receive the sentinel instead of recursing. Once the real module finishes loading, the sentinel is populated via Object.assign() so any references obtained during the circular load phase see the real exports. This safely breaks the infinite recursion without changing the eager evaluation semantics of constant exports. The generic constraint <T> was also tightened to <T extends object> to ensure Object.assign() is type-safe.
  • What changed:
    • src/plugin-sdk/facade-runtime.ts: Added sentinel object logic in loadBundledPluginPublicSurfaceModuleSync and tightened generic constraint to <T extends object>.
    • src/test-utils/bundled-plugin-public-surface.ts: Propagated the <T extends object> constraint to loadBundledPluginPublicSurfaceSync and loadBundledPluginTestApiSync.
  • What did NOT change (scope boundary): No changes to actual plugin implementations (xai, sglang, vllm) or their dependency graphs. No changes to the Jiti loader configuration, the resolveFacadeModuleLocation function, or the openBoundaryFileSync security boundary. The fix is purely at the infrastructure level (facade loader) to gracefully handle circular facade references during module evaluation.

Reproduction

  1. Run pnpm build on any commit after 8e0ab35b0e.
  2. Start the gateway: node dist/index.js gateway --port 18789.
  3. Observe stderr — xai, sglang, and vllm fail in a retry loop with RangeError: Maximum call stack size exceeded.

Risk / Mitigation

  • Risk: The sentinel object is temporarily empty during the synchronous Jiti load. If a circular dependency attempts to read a property from the facade module during the load phase (rather than just capturing a reference to the module object itself), it would get undefined.
  • Mitigation: This risk is mitigated because the circular dependencies in these plugins only capture references to the facade module or its exports during evaluation; they do not execute logic that reads the constants until after the module graph is fully loaded. Furthermore, Object.assign() ensures that all captured references to the sentinel object are populated with the actual exports immediately after the Jiti load completes. Local testing confirms all three plugins load successfully.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway
  • Plugin SDK (src/plugin-sdk)

Linked Issue/PR

Fixes #57394

Changed files

  • src/plugin-sdk/facade-runtime.ts (modified, +20/-4)
  • src/test-utils/bundled-plugin-public-surface.ts (modified, +2/-2)

PR #57508: fix(facade-runtime): add recursion guard to facade module loader to prevent infinite stack overflow

Description (problem / solution / changelog)

Summary

  • Problem: The xai, sglang, and vllm plugins crash on every load attempt with RangeError: Maximum call stack size exceeded. The gateway retries loading these plugins every ~8 minutes, each time hitting the same stack overflow. This occurs in src/plugin-sdk/facade-runtime.ts at loadBundledPluginPublicSurfaceModuleSync() (line 171-206).
  • Root Cause: The facade lazy-loading pattern introduced in 8e0ab35b0e handles function exports correctly (wrapped in closures, loaded on first call) but constant exports eagerly call loadFacadeModule() at module-evaluation time. When the constant initializer triggers loadFacadeModule() → Jiti sync load of extensions/{xai,sglang,vllm}/api.ts, any transitive dependency that imports back to openclaw/plugin-sdk/{xai,sglang,vllm} (even indirectly) re-enters loadBundledPluginPublicSurfaceModuleSync(). The loadedFacadeModules cache is only set after the Jiti load returns (line 204), so the re-entrant call finds no cache entry and recurses until stack overflow.
  • Fix: Set a sentinel object in the loadedFacadeModules cache before the Jiti load begins. Re-entrant calls receive the sentinel instead of recursing. Once the real module finishes loading, Object.assign() back-fills the sentinel so any references captured during the circular load phase see the final exports. Both the Jiti load and the Object.assign() back-fill are wrapped in try/catch: on failure the sentinel is removed from the cache so that subsequent retry attempts re-execute the load instead of silently returning an empty object. The function returns the sentinel (not the raw loaded module) to guarantee a single object identity for all callers, including those that captured a reference during the circular load phase.
  • What changed:
    • src/plugin-sdk/facade-runtime.ts: Added sentinel object logic with try/catch guard (covering both Jiti load and Object.assign back-fill) in loadBundledPluginPublicSurfaceModuleSync, tightened generic constraint to <T extends object>.
    • src/test-utils/bundled-plugin-public-surface.ts: Propagated the <T extends object> constraint to loadBundledPluginPublicSurfaceSync and loadBundledPluginTestApiSync.
    • src/plugin-sdk/facade-runtime.test.ts: Added two test cases — sentinel object identity consistency on repeated calls, and cache cleanup on load failure ensuring retries re-execute.
  • What did NOT change (scope boundary): No changes to actual plugin implementations (xai, sglang, vllm) or their dependency graphs. No changes to the Jiti loader configuration, the resolveFacadeModuleLocation function, or the openBoundaryFileSync security boundary. The fix is purely at the infrastructure level (facade loader) to gracefully handle circular facade references during module evaluation.

Reproduction

  1. Upgrade to 2026.3.28 or later.
  2. Enable any of the xai, sglang, or vllm LLM provider plugins.
  3. Start the gateway — observe RangeError: Maximum call stack size exceeded in logs every ~8 minutes.

Risk / Mitigation

  • Risk: The sentinel object is temporarily empty during the Jiti load phase. If a circular caller reads a constant export from the sentinel before the load completes, it would get undefined.
  • Mitigation: The circular dependencies in these plugins only capture references to the facade module or its exports during evaluation; they do not execute logic that reads the constants until after the module graph is fully loaded. The try/catch ensures that if the Jiti load fails for any reason, the sentinel is removed from the cache so subsequent retries re-execute the load cleanly. Returning the sentinel (instead of the raw loaded object) guarantees a single object identity for all callers.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway
  • Plugin SDK (src/plugin-sdk)

Linked Issue/PR

Fixes #57394

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/plugin-sdk/facade-runtime.test.ts (modified, +90/-0)
  • src/plugin-sdk/facade-runtime.ts (modified, +20/-4)
  • src/test-utils/bundled-plugin-public-surface.ts (modified, +2/-2)

Code Example

[plugins] xai failed to load from extensions/xai/index.ts: RangeError: Maximum call stack size exceeded
[plugins] sglang failed to load from extensions/sglang/index.ts: RangeError: Maximum call stack size exceeded
[plugins] vllm failed to load from extensions/vllm/index.ts: RangeError: Maximum call stack size exceeded

---

// Functions — lazy, OK:
export const buildXaiProvider: F["buildXaiProvider"] = ((...args) =>
  loadFacadeModule()["buildXaiProvider"](...args)) as F["buildXaiProvider"];

// Constants — eager, crashes:
export const XAI_BASE_URL: F["XAI_BASE_URL"] = loadFacadeModule()["XAI_BASE_URL"];

---

--- a/src/plugin-sdk/facade-runtime.ts
+++ b/src/plugin-sdk/facade-runtime.ts
@@ -168,7 +168,7 @@
-export function loadBundledPluginPublicSurfaceModuleSync<T>(params: {
+export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(params: {
   dirName: string;
   artifactBasename: string;
 }): T {
@@ -200,7 +200,17 @@
   fs.closeSync(opened.fd);

+  const sentinel = {} as T;
+  loadedFacadeModules.set(location.modulePath, sentinel);
   const loaded = getJiti(location.modulePath)(location.modulePath) as T;
+  Object.assign(sentinel, loaded);
   loadedFacadeModules.set(location.modulePath, loaded);
   return loaded;
 }
RAW_BUFFERClick to expand / collapse

Bug Report

Description

After 8e0ab35b0e ("refactor(plugins): decouple bundled plugin runtime loading"), the xai, sglang, and vllm plugins crash on every load attempt with RangeError: Maximum call stack size exceeded. The gateway retries loading these plugins every ~8 minutes, each time hitting the same stack overflow.

Environment

  • OpenClaw version: 2026.3.28
  • Platform: macOS 14.7.6 (Darwin 23.6.0), x86_64
  • Node: 25.6.0
  • Host: macOS LaunchDaemon

Steps to Reproduce

  1. Upgrade to any commit after 8e0ab35b0e
  2. Run pnpm build
  3. Start the gateway: node dist/index.js gateway --port 18789
  4. Observe stderr — xai, sglang, and vllm fail in a retry loop:
[plugins] xai failed to load from extensions/xai/index.ts: RangeError: Maximum call stack size exceeded
[plugins] sglang failed to load from extensions/sglang/index.ts: RangeError: Maximum call stack size exceeded
[plugins] vllm failed to load from extensions/vllm/index.ts: RangeError: Maximum call stack size exceeded

Root Cause

The facade lazy-loading pattern introduced in 8e0ab35b0e handles function exports correctly (wrapped in closures, loaded on first call) but constant exports eagerly call loadFacadeModule() at module-evaluation time:

// Functions — lazy, OK:
export const buildXaiProvider: F["buildXaiProvider"] = ((...args) =>
  loadFacadeModule()["buildXaiProvider"](...args)) as F["buildXaiProvider"];

// Constants — eager, crashes:
export const XAI_BASE_URL: F["XAI_BASE_URL"] = loadFacadeModule()["XAI_BASE_URL"];

When the constant initializer triggers loadFacadeModule() → Jiti sync load of extensions/xai/api.ts, any transitive dependency that imports back to openclaw/plugin-sdk/xai (even indirectly) re-enters loadBundledPluginPublicSurfaceModuleSync(). The loadedFacadeModules cache is only set after the Jiti load returns (line 204 of facade-runtime.ts), so the re-entrant call finds no cache entry and recurses until stack overflow.

The xai, sglang, and vllm extensions are affected because their api.ts files export constants and their dependency graphs have circular paths back through the SDK facades.

Proposed Fix

Set a sentinel object in the loadedFacadeModules cache before the Jiti load begins. Re-entrant calls receive the sentinel instead of recursing. Once the real module finishes loading, the sentinel is populated via Object.assign() so any references obtained during the circular load phase see the real exports.

--- a/src/plugin-sdk/facade-runtime.ts
+++ b/src/plugin-sdk/facade-runtime.ts
@@ -168,7 +168,7 @@
-export function loadBundledPluginPublicSurfaceModuleSync<T>(params: {
+export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(params: {
   dirName: string;
   artifactBasename: string;
 }): T {
@@ -200,7 +200,17 @@
   fs.closeSync(opened.fd);

+  const sentinel = {} as T;
+  loadedFacadeModules.set(location.modulePath, sentinel);
   const loaded = getJiti(location.modulePath)(location.modulePath) as T;
+  Object.assign(sentinel, loaded);
   loadedFacadeModules.set(location.modulePath, loaded);
   return loaded;
 }

Also requires propagating T extends object to callers in src/test-utils/bundled-plugin-public-surface.ts.

Patch branch

Local branch fix/facade-runtime-recursion-guard has the complete fix. All pre-commit checks pass (tsgo, oxlint, conflict markers, host-env policy, webhook auth, pairing scope). Manually verified: all three plugins load successfully after the fix.

Additional Context

This upgrade also exposed two other operational issues (not code bugs):

  1. Stale dist/ after git pull — the compiled plugin SDK .d.ts files were missing exports for createSubsystemLogger, registerCliBackend, and createResolvedDirectoryEntriesLister, which broke amazon-bedrock, anthropic, google, and matrix plugins. Fixed by running pnpm build.

  2. Stale nutrient-openclaw config entry~/.openclaw/openclaw.json referenced a removed plugin. Fixed by removing the entry.

🤖 Generated with Claude Code

extent analysis

Fix Plan

To resolve the RangeError: Maximum call stack size exceeded issue, apply the following steps:

  1. Update facade-runtime.ts:
    • Set a sentinel object in the loadedFacadeModules cache before the Jiti load begins.
    • Populate the sentinel with the real module exports once the load is complete.
export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(params: {
  dirName: string;
  artifactBasename: string;
}): T {
  // ...
  const sentinel = {} as T;
  loadedFacadeModules.set(location.modulePath, sentinel);
  const loaded = getJiti(location.modulePath)(location.modulePath) as T;
  Object.assign(sentinel, loaded);
  loadedFacadeModules.set(location.modulePath, loaded);
  return loaded;
}
  1. Update type annotations:
    • Propagate T extends object to callers in src/test-utils/bundled-plugin-public-surface.ts.

Verification

After applying the fix:

  • Run pnpm build to ensure the compiled plugin SDK is up-to-date.
  • Start the gateway: node dist/index.js gateway --port 18789.
  • Verify that the xai, sglang, and vllm plugins load successfully without crashing due to the RangeError.

Extra Tips

  • Regularly run pnpm build after pulling changes to prevent issues with stale dist/ files.
  • Keep the ~/.openclaw/openclaw.json config entry up-to-date by removing references to removed plugins.

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