From 4bfa800cc7c2ab510f5df23dcb84c384bf5539a8 Mon Sep 17 00:00:00 2001 From: Josh Lehman Date: Sun, 8 Mar 2026 11:56:01 -0700 Subject: [PATCH] fix: share context engine registry across bundled chunks (#40115) Merged via squash. Prepared head SHA: 6af4820b7d0ea64d96f2f894ef3b0e5750b776aa Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman --- CHANGELOG.md | 1 + src/context-engine/context-engine.test.ts | 13 +++++++++++ src/context-engine/registry.ts | 28 +++++++++++++++++++---- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 904126a13..def915d48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai - Sessions/model switch: clear stale cached `contextTokens` when a session changes models so status and runtime paths recompute against the active model window. (#38044) thanks @yuweuii. - ACP/session history: persist transcripts for successful ACP child runs, preserve exact transcript text, record ACP spawned-session lineage, and keep spawn-time transcript-path persistence best-effort so history storage failures do not block execution. (#40137) thanks @mbelinky. - Browser/CDP: normalize loopback direct WebSocket CDP URLs back to HTTP(S) for `/json/*` tab operations so local `ws://` / `wss://` profiles can still list, focus, open, and close tabs after the new direct-WS support lands. (#31085) Thanks @shrey150. +- Context engine registry/bundled builds: share the registry state through a `globalThis` singleton so duplicated bundled module copies can resolve engines registered by each other at runtime, with regression coverage for duplicate-module imports. (#40115) thanks @jalehman. ## 2026.3.7 diff --git a/src/context-engine/context-engine.test.ts b/src/context-engine/context-engine.test.ts index e6788d2f8..91b9ffac5 100644 --- a/src/context-engine/context-engine.test.ts +++ b/src/context-engine/context-engine.test.ts @@ -198,6 +198,19 @@ describe("Registry tests", () => { expect(getContextEngineFactory("reg-overwrite")).toBe(factory2); expect(getContextEngineFactory("reg-overwrite")).not.toBe(factory1); }); + + it("shares registered engines across duplicate module copies", async () => { + const registryUrl = new URL("./registry.ts", import.meta.url).href; + const suffix = Date.now().toString(36); + const first = await import(/* @vite-ignore */ `${registryUrl}?copy=${suffix}-a`); + const second = await import(/* @vite-ignore */ `${registryUrl}?copy=${suffix}-b`); + + const engineId = `dup-copy-${suffix}`; + const factory = () => new MockContextEngine(); + first.registerContextEngine(engineId, factory); + + expect(second.getContextEngineFactory(engineId)).toBe(factory); + }); }); // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/context-engine/registry.ts b/src/context-engine/registry.ts index 49bf34bfb..d73266c62 100644 --- a/src/context-engine/registry.ts +++ b/src/context-engine/registry.ts @@ -12,27 +12,45 @@ export type ContextEngineFactory = () => ContextEngine | Promise; // Registry (module-level singleton) // --------------------------------------------------------------------------- -const _engines = new Map(); +const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState"); + +type ContextEngineRegistryState = { + engines: Map; +}; + +// Keep context-engine registrations process-global so duplicated dist chunks +// still share one registry map at runtime. +function getContextEngineRegistryState(): ContextEngineRegistryState { + const globalState = globalThis as typeof globalThis & { + [CONTEXT_ENGINE_REGISTRY_STATE]?: ContextEngineRegistryState; + }; + if (!globalState[CONTEXT_ENGINE_REGISTRY_STATE]) { + globalState[CONTEXT_ENGINE_REGISTRY_STATE] = { + engines: new Map(), + }; + } + return globalState[CONTEXT_ENGINE_REGISTRY_STATE]; +} /** * Register a context engine implementation under the given id. */ export function registerContextEngine(id: string, factory: ContextEngineFactory): void { - _engines.set(id, factory); + getContextEngineRegistryState().engines.set(id, factory); } /** * Return the factory for a registered engine, or undefined. */ export function getContextEngineFactory(id: string): ContextEngineFactory | undefined { - return _engines.get(id); + return getContextEngineRegistryState().engines.get(id); } /** * List all registered engine ids. */ export function listContextEngineIds(): string[] { - return [..._engines.keys()]; + return [...getContextEngineRegistryState().engines.keys()]; } // --------------------------------------------------------------------------- @@ -55,7 +73,7 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise