import type { OpenClawConfig } from "../config/config.js"; import { defaultSlotIdForKey } from "../plugins/slots.js"; import type { ContextEngine } from "./types.js"; /** * A factory that creates a ContextEngine instance. * Supports async creation for engines that need DB connections etc. */ export type ContextEngineFactory = () => ContextEngine | Promise; export type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string }; type RegisterContextEngineForOwnerOptions = { allowSameOwnerRefresh?: boolean; }; // --------------------------------------------------------------------------- // Registry (module-level singleton) // --------------------------------------------------------------------------- const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState"); const CORE_CONTEXT_ENGINE_OWNER = "core"; const PUBLIC_CONTEXT_ENGINE_OWNER = "public-sdk"; type ContextEngineRegistryState = { engines: Map< string, { factory: ContextEngineFactory; owner: string; } >; }; // 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]; } function requireContextEngineOwner(owner: string): string { const normalizedOwner = owner.trim(); if (!normalizedOwner) { throw new Error( `registerContextEngineForOwner: owner must be a non-empty string, got ${JSON.stringify(owner)}`, ); } return normalizedOwner; } /** * Register a context engine implementation under an explicit trusted owner. */ export function registerContextEngineForOwner( id: string, factory: ContextEngineFactory, owner: string, opts?: RegisterContextEngineForOwnerOptions, ): ContextEngineRegistrationResult { const normalizedOwner = requireContextEngineOwner(owner); const registry = getContextEngineRegistryState().engines; const existing = registry.get(id); if ( id === defaultSlotIdForKey("contextEngine") && normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER ) { return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER }; } if (existing && existing.owner !== normalizedOwner) { return { ok: false, existingOwner: existing.owner }; } if (existing && opts?.allowSameOwnerRefresh !== true) { return { ok: false, existingOwner: existing.owner }; } registry.set(id, { factory, owner: normalizedOwner }); return { ok: true }; } /** * Public SDK entry point for third-party registrations. * * This path is intentionally unprivileged: it cannot claim core-owned ids and * it cannot safely refresh an existing registration because the caller's * identity is not authenticated. */ export function registerContextEngine( id: string, factory: ContextEngineFactory, ): ContextEngineRegistrationResult { return registerContextEngineForOwner(id, factory, PUBLIC_CONTEXT_ENGINE_OWNER); } /** * Return the factory for a registered engine, or undefined. */ export function getContextEngineFactory(id: string): ContextEngineFactory | undefined { return getContextEngineRegistryState().engines.get(id)?.factory; } /** * List all registered engine ids. */ export function listContextEngineIds(): string[] { return [...getContextEngineRegistryState().engines.keys()]; } // --------------------------------------------------------------------------- // Resolution // --------------------------------------------------------------------------- /** * Resolve which ContextEngine to use based on plugin slot configuration. * * Resolution order: * 1. `config.plugins.slots.contextEngine` (explicit slot override) * 2. Default slot value ("legacy") * * Throws if the resolved engine id has no registered factory. */ export async function resolveContextEngine(config?: OpenClawConfig): Promise { const slotValue = config?.plugins?.slots?.contextEngine; const engineId = typeof slotValue === "string" && slotValue.trim() ? slotValue.trim() : defaultSlotIdForKey("contextEngine"); const entry = getContextEngineRegistryState().engines.get(engineId); if (!entry) { throw new Error( `Context engine "${engineId}" is not registered. ` + `Available engines: ${listContextEngineIds().join(", ") || "(none)"}`, ); } return entry.factory(); }