Skip to content

Commit 4bfa800

Browse files
authored
fix: share context engine registry across bundled chunks (#40115)
Merged via squash. Prepared head SHA: 6af4820 Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com> Reviewed-by: @jalehman
1 parent 9914b48 commit 4bfa800

3 files changed

Lines changed: 37 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ Docs: https://docs.openclaw.ai
2828
- 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.
2929
- 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.
3030
- 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.
31+
- 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.
3132

3233
## 2026.3.7
3334

src/context-engine/context-engine.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,19 @@ describe("Registry tests", () => {
198198
expect(getContextEngineFactory("reg-overwrite")).toBe(factory2);
199199
expect(getContextEngineFactory("reg-overwrite")).not.toBe(factory1);
200200
});
201+
202+
it("shares registered engines across duplicate module copies", async () => {
203+
const registryUrl = new URL("./registry.ts", import.meta.url).href;
204+
const suffix = Date.now().toString(36);
205+
const first = await import(/* @vite-ignore */ `${registryUrl}?copy=${suffix}-a`);
206+
const second = await import(/* @vite-ignore */ `${registryUrl}?copy=${suffix}-b`);
207+
208+
const engineId = `dup-copy-${suffix}`;
209+
const factory = () => new MockContextEngine();
210+
first.registerContextEngine(engineId, factory);
211+
212+
expect(second.getContextEngineFactory(engineId)).toBe(factory);
213+
});
201214
});
202215

203216
// ═══════════════════════════════════════════════════════════════════════════

src/context-engine/registry.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,27 +12,45 @@ export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
1212
// Registry (module-level singleton)
1313
// ---------------------------------------------------------------------------
1414

15-
const _engines = new Map<string, ContextEngineFactory>();
15+
const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState");
16+
17+
type ContextEngineRegistryState = {
18+
engines: Map<string, ContextEngineFactory>;
19+
};
20+
21+
// Keep context-engine registrations process-global so duplicated dist chunks
22+
// still share one registry map at runtime.
23+
function getContextEngineRegistryState(): ContextEngineRegistryState {
24+
const globalState = globalThis as typeof globalThis & {
25+
[CONTEXT_ENGINE_REGISTRY_STATE]?: ContextEngineRegistryState;
26+
};
27+
if (!globalState[CONTEXT_ENGINE_REGISTRY_STATE]) {
28+
globalState[CONTEXT_ENGINE_REGISTRY_STATE] = {
29+
engines: new Map<string, ContextEngineFactory>(),
30+
};
31+
}
32+
return globalState[CONTEXT_ENGINE_REGISTRY_STATE];
33+
}
1634

1735
/**
1836
* Register a context engine implementation under the given id.
1937
*/
2038
export function registerContextEngine(id: string, factory: ContextEngineFactory): void {
21-
_engines.set(id, factory);
39+
getContextEngineRegistryState().engines.set(id, factory);
2240
}
2341

2442
/**
2543
* Return the factory for a registered engine, or undefined.
2644
*/
2745
export function getContextEngineFactory(id: string): ContextEngineFactory | undefined {
28-
return _engines.get(id);
46+
return getContextEngineRegistryState().engines.get(id);
2947
}
3048

3149
/**
3250
* List all registered engine ids.
3351
*/
3452
export function listContextEngineIds(): string[] {
35-
return [..._engines.keys()];
53+
return [...getContextEngineRegistryState().engines.keys()];
3654
}
3755

3856
// ---------------------------------------------------------------------------
@@ -55,7 +73,7 @@ export async function resolveContextEngine(config?: OpenClawConfig): Promise<Con
5573
? slotValue.trim()
5674
: defaultSlotIdForKey("contextEngine");
5775

58-
const factory = _engines.get(engineId);
76+
const factory = getContextEngineRegistryState().engines.get(engineId);
5977
if (!factory) {
6078
throw new Error(
6179
`Context engine "${engineId}" is not registered. ` +

0 commit comments

Comments
 (0)