fix(plugin-sdk): resolve facade post-load re-entry (#61286)

This commit is contained in:
Vincent Koc 2026-04-05 11:25:36 +01:00 committed by GitHub
parent 4559ece355
commit fd0cc90427
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 59 additions and 5 deletions

View File

@ -139,7 +139,6 @@ Docs: https://docs.openclaw.ai
- Heartbeat: skip wake delivery when the target session lane is already busy so the pending event is retried instead of getting drained too early. (#40526) Thanks @lucky7323.
- Plugin SDK/context engines: export the missing context-engine result and subagent lifecycle types from `openclaw/plugin-sdk` so context engine plugins can type `ContextEngine` implementations without local workarounds. (#61251) Thanks @DaevMithran.
- Agents/errors: surface an explicit disk-full message when local session or transcript writes fail with `ENOSPC`/`disk full`, so those runs stop degrading into opaque `NO_REPLY`-style failures. Thanks @vincentkoc.
<<<<<<< HEAD
- Google Gemini CLI models: add forward-compat support for stable `gemini-2.5-*` model ids by letting the bundled CLI provider clone them from Google templates, so `gemini-2.5-flash-lite` and related configured models stop showing up as missing. (#35274) Thanks @mySebbe.
- Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly `reasoning:stream`, so hidden `<think>` traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc.
- Feishu/reasoning: only expose streamed reasoning previews when the session is explicitly `reasoning:stream`, so hidden reasoning traces do not surface on normal streaming sessions. Thanks @vincentkoc.
@ -153,6 +152,7 @@ Docs: https://docs.openclaw.ai
- Agents/Claude CLI/security: force host-managed Claude CLI backdoor runs to `--setting-sources user`, even under custom backend arg overrides, so repo-local `.claude` project/local settings, hooks, and plugin discovery do not silently execute inside non-interactive OpenClaw sessions. Thanks @vincentkoc.
- Agents/Claude CLI/images: reuse stable hydrated image file paths and preserve shared media extensions like HEIC when passing image refs to local CLI runs, so Claude CLI image prompts stop thrashing KV cache prefixes and oddball image formats do not fall back to `.bin`. Thanks @vincentkoc.
- Google Gemini CLI auth: detect personal OAuth mode from local Gemini settings and skip Code Assist project discovery for those logins, so personal Google accounts stop failing with `loadCodeAssist 400 Bad Request`. (#49226) Thanks @bobworrall.
- Plugin SDK/facades: back-fill bundled plugin facade sentinels before plugin-id tracking re-enters config loading, so CLI/provider startup no longer crashes with `shouldNormalizeGoogleProviderConfig is not a function` or other empty-facade reads during bundled plugin re-entry. Thanks @adam91holt.
## 2026.4.2

View File

@ -75,6 +75,7 @@ afterEach(() => {
vi.restoreAllMocks();
clearRuntimeConfigSnapshot();
resetFacadeRuntimeStateForTest();
vi.doUnmock("../plugins/manifest-registry.js");
delete (globalThis as typeof globalThis & Record<string, unknown>)[FACADE_RUNTIME_GLOBAL];
if (originalBundledPluginsDir === undefined) {
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
@ -137,6 +138,56 @@ describe("plugin-sdk facade runtime", () => {
expect(loaded.marker).toBe("circular-ok");
});
it("back-fills the sentinel before post-load facade tracking re-enters", async () => {
const dir = createBundledPluginDir("openclaw-facade-post-load-", "post-load-ok");
const reentryMarkers: Array<string | undefined> = [];
vi.resetModules();
vi.doMock("../plugins/manifest-registry.js", () => ({
loadPluginManifestRegistry: vi.fn(() => {
const load = (
globalThis as typeof globalThis & {
[FACADE_RUNTIME_GLOBAL]?: typeof loadBundledPluginPublicSurfaceModuleSync;
}
)[FACADE_RUNTIME_GLOBAL];
if (typeof load !== "function") {
throw new Error("missing facade runtime test loader");
}
const reentered = load<{ marker?: string }>({
dirName: "demo",
artifactBasename: "api.js",
});
reentryMarkers.push(reentered.marker);
return {
plugins: [
{
id: "demo",
rootDir: path.join(dir, "demo"),
origin: "bundled",
},
],
};
}),
}));
const facadeRuntime = await import("./facade-runtime.js");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
(globalThis as typeof globalThis & Record<string, unknown>)[FACADE_RUNTIME_GLOBAL] =
facadeRuntime.loadBundledPluginPublicSurfaceModuleSync;
const loaded = facadeRuntime.loadBundledPluginPublicSurfaceModuleSync<{ marker: string }>({
dirName: "demo",
artifactBasename: "api.js",
});
expect(loaded.marker).toBe("post-load-ok");
expect(reentryMarkers).toEqual(["post-load-ok"]);
expect(facadeRuntime.listImportedBundledPluginFacadeIds()).toEqual(["demo"]);
facadeRuntime.resetFacadeRuntimeStateForTest();
vi.doUnmock("../plugins/manifest-registry.js");
vi.resetModules();
});
it("clears the cache on load failure so retries re-execute", () => {
const dir = createThrowingPluginDir("openclaw-facade-throw-");
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = dir;
@ -148,7 +199,7 @@ describe("plugin-sdk facade runtime", () => {
}),
).toThrow("plugin load failure");
expect(listImportedBundledPluginFacadeIds()).toEqual(["bad"]);
expect(listImportedBundledPluginFacadeIds()).toEqual([]);
// A second call must also throw (not return a stale empty sentinel).
expect(() =>

View File

@ -365,11 +365,14 @@ export function loadBundledPluginPublicSurfaceModuleSync<T extends object>(param
let loaded: T;
try {
// Track the owning plugin once module evaluation begins. Facade top-level
// code may have already executed even if the module later throws.
loadedFacadePluginIds.add(resolveTrackedFacadePluginId(params));
loaded = getJiti(location.modulePath)(location.modulePath) as T;
// Back-fill the sentinel before resolving plugin ownership. That lookup can
// trigger config loading, plugin auto-enable, and other facade reads that
// re-enter this loader for the same module path.
Object.assign(sentinel, loaded);
// Track the owning plugin after the module exports are visible through the
// sentinel, so re-entrant callers never observe an empty facade object.
loadedFacadePluginIds.add(resolveTrackedFacadePluginId(params));
} catch (err) {
loadedFacadeModules.delete(location.modulePath);
throw err;