Download src/context-engine/registry.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 30 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/context-engine/registry.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/context-engine/registry.ts
-
curl -L -o registry.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/context-engine/registry.ts
30 kB
| // Context-engine registry owns engine registration, resolution, compatibility, and quarantine. | |
| import { sanitizeForLog } from "../../packages/terminal-core/src/ansi.js"; | |
| import type { OpenClawConfig } from "../config/types.js"; | |
| import { runPluginCleanup } from "../plugins/plugin-instance-scope.js"; | |
| import type { | |
| ContextEngineFactory, | |
| ContextEngineFactoryContext, | |
| ContextEngineRegistration, | |
| ContextEngineRegistrationLifecycle, | |
| } from "../plugins/registry-contribution-types.js"; | |
| import type { PluginRegistry } from "../plugins/registry-types.js"; | |
| import { getActivePluginRegistry, requireActivePluginRegistry } from "../plugins/runtime.js"; | |
| import { defaultSlotIdForKey } from "../plugins/slots.js"; | |
| import { getAsyncWorkSignal } from "../shared/async-work-scope.js"; | |
| import { createDeferredCore } from "../shared/deferred.js"; | |
| import { resolveGlobalSingleton } from "../shared/global-singleton.js"; | |
| import { | |
| inheritRuntimeCompactionDelegate, | |
| markRuntimeCompactionDelegate, | |
| } from "./compaction-watchdog.js"; | |
| import { contextEngineAbortSignal, isContextEngineAbortRejection } from "./context-engine-abort.js"; | |
| import { | |
| clearPersistedContextEngineQuarantineForProcess, | |
| listPersistedContextEngineQuarantines, | |
| recordPersistedContextEngineQuarantine, | |
| } from "./quarantine-health.js"; | |
| import { pluginIdFromContextEngineOwner } from "./registry-adoption.js"; | |
| import { | |
| describeResolvedContextEngineContractError, | |
| projectContextEngineHostParams, | |
| } from "./registry-contract.js"; | |
| import { resolveEffectiveContextEngineId } from "./registry-selection.js"; | |
| import { | |
| recordContextEngineRegistrationSource, | |
| createContextEngineWithResources, | |
| disposeContextEngineSources, | |
| resolveContextEngineFactory, | |
| retainLogicalTurnContextEngineSources, | |
| runContextEngineFactoryResolution, | |
| type ContextEngineFactoryResources, | |
| } from "./registry.resources.js"; | |
| import type { | |
| BootstrapResult, | |
| ContextEngine, | |
| ContextEngineMaintenanceResult, | |
| IngestBatchResult, | |
| IngestResult, | |
| } from "./types.js"; | |
| export type { ContextEngineFactory } from "../plugins/registry-contribution-types.js"; | |
| /** | |
| * Runtime context passed to context engine factories during resolution. | |
| * Provides config and path information so plugins can initialize engines | |
| * without fragile workarounds. | |
| */ | |
| type ContextEngineRegistrationResult = { ok: true } | { ok: false; existingOwner: string }; | |
| type RegisterContextEngineForOwnerOptions = { | |
| allowSameOwnerRefresh?: boolean; | |
| lifecycle?: ContextEngineRegistrationLifecycle; | |
| }; | |
| type GuardedContextEngineMethodName = Exclude<keyof ContextEngine, "info" | "dispose">; | |
| type GuardedContextEngineMethod = (...args: never[]) => unknown; | |
| const GUARDED_CONTEXT_ENGINE_METHODS = new Set<PropertyKey>( | |
| "bootstrap maintain ingest ingestBatch afterTurn commitTurn assemble compact prepareSubagentSpawn onSubagentEnded".split( | |
| " ", | |
| ), | |
| ); | |
| type ResolvedContextEngineMetadata = { | |
| owner: string; | |
| engineId: string; | |
| sourceEngine?: ContextEngine; | |
| source?: ContextEngineFactoryResources; | |
| ownsSource?: boolean; | |
| }; | |
| const resolvedEngineMetadata = new WeakMap<ContextEngine, ResolvedContextEngineMetadata>(); | |
| function inheritCompactionWatchdogOwnership( | |
| property: PropertyKey, | |
| source: GuardedContextEngineMethod, | |
| wrapped: GuardedContextEngineMethod, | |
| ): GuardedContextEngineMethod { | |
| if (property !== "compact") { | |
| return wrapped; | |
| } | |
| // SAFETY: the compact property narrows both functions to the ContextEngine compact contract. | |
| const compact = source as ContextEngine["compact"]; | |
| // SAFETY: guarded compact wrappers preserve the source method's single-parameter contract. | |
| const wrappedCompact = wrapped as ContextEngine["compact"]; | |
| return inheritRuntimeCompactionDelegate(compact, wrappedCompact); | |
| } | |
| function wrapResolvedContextEngine( | |
| rawEngine: ContextEngine, | |
| metadata: ResolvedContextEngineMetadata & { | |
| factory: ContextEngineFactory; | |
| defaultEngineId?: string; | |
| factoryCtx?: ContextEngineFactoryContext; | |
| }, | |
| ): ContextEngine { | |
| let disposal: Promise<void> | undefined; | |
| const source = metadata.source; | |
| const engine = source?.wrap(rawEngine) ?? rawEngine; | |
| const fallback = | |
| metadata.defaultEngineId && | |
| metadata.factoryCtx && | |
| metadata.engineId !== metadata.defaultEngineId | |
| ? { defaultEngineId: metadata.defaultEngineId, factoryCtx: metadata.factoryCtx } | |
| : undefined; | |
| let fallbackEnginePromise: Promise<ContextEngine> | undefined; | |
| let resolvedFallbackEngine: ContextEngine | undefined; | |
| const getFallbackEngine = fallback | |
| ? async () => { | |
| if (disposal) { | |
| throw new Error("Context engine has been disposed"); | |
| } | |
| const resolve = () => | |
| resolveDefaultContextEngine(fallback.defaultEngineId, fallback.factoryCtx); | |
| // Failed factories return before cleanup; capture that work in this engine's source owner. | |
| return await (fallbackEnginePromise ??= (source ? source.run(resolve) : resolve()).then( | |
| (resolved) => { | |
| resolvedFallbackEngine = resolved; | |
| return resolved; | |
| }, | |
| )); | |
| } | |
| : undefined; | |
| const disposeOwned = () => { | |
| if (disposal) { | |
| return disposal; | |
| } | |
| const completion = createDeferredCore(); | |
| disposal = completion.promise; | |
| void (async () => { | |
| // Join only a fallback already admitted before closure; disposal must never create one. | |
| const fallbackResult: PromiseSettledResult<ContextEngine | undefined> = | |
| fallbackEnginePromise && !resolvedFallbackEngine | |
| ? (await Promise.allSettled([fallbackEnginePromise]))[0]! | |
| : { status: "fulfilled", value: resolvedFallbackEngine }; | |
| const fallbackEngine = | |
| fallbackResult.status === "fulfilled" ? fallbackResult.value : undefined; | |
| const sources = metadata.ownsSource && source ? [source] : []; | |
| const shared = fallbackEngine && hasSameContextEngineInstance(wrapped, fallbackEngine); | |
| const fallbackSource = fallbackEngine && resolvedEngineMetadata.get(fallbackEngine)?.source; | |
| if (shared && fallbackSource) { | |
| sources.push(fallbackSource); | |
| } | |
| // The fallback is a child of this factory; start its disposer before closing the parent signal. | |
| const fallbackCleanup = (async () => { | |
| if (!shared) { | |
| await fallbackEngine?.dispose?.(); | |
| } | |
| })(); | |
| // Shared raw engines dispose once with both source claims held; independent cleanup all runs. | |
| const results = await Promise.allSettled([ | |
| (async () => { | |
| if (source && !metadata.ownsSource) { | |
| await source.runCleanup(() => rawEngine.dispose?.()); | |
| } else { | |
| await disposeContextEngineSources(rawEngine, sources, () => | |
| source | |
| ? rawEngine.dispose?.() | |
| : runPluginCleanup(metadata.factory, () => rawEngine.dispose?.()), | |
| ); | |
| } | |
| })(), | |
| fallbackCleanup, | |
| ]); | |
| for (const result of [...results, fallbackResult]) { | |
| if (result.status === "rejected") { | |
| throw result.reason; | |
| } | |
| } | |
| })().then(completion.resolve, completion.reject); | |
| return disposal; | |
| }; | |
| // A fresh target keeps Proxy invariants compatible with frozen engines and private getters. | |
| const wrapped = new Proxy( | |
| Object.create(engine, { info: { get: () => engine.info } }) as ContextEngine, | |
| { | |
| get(_target, property) { | |
| if (property === "dispose" && (source || fallback)) { | |
| return disposeOwned; | |
| } | |
| if (property === "info") { | |
| if (!fallback || !getContextEngineQuarantine(metadata.engineId)) { | |
| return engine.info; | |
| } | |
| return ( | |
| resolvedFallbackEngine?.info ?? { | |
| id: fallback.defaultEngineId, | |
| name: | |
| fallback.defaultEngineId === "legacy" | |
| ? "Legacy Context Engine" | |
| : `${fallback.defaultEngineId} Context Engine`, | |
| } | |
| ); | |
| } | |
| // Disposal keeps its registered owner while ordinary methods retain their normal fences. | |
| const invokeMember = <T>(run: () => T): T => | |
| property === "dispose" ? runPluginCleanup(metadata.factory, run) : run(); | |
| const method = invokeMember(() => Reflect.get(engine, property, engine)); | |
| if (typeof method !== "function") { | |
| return method; | |
| } | |
| if (!GUARDED_CONTEXT_ENGINE_METHODS.has(property)) { | |
| return (...args: unknown[]) => invokeMember(() => Reflect.apply(method, engine, args)); | |
| } | |
| const methodName = property as GuardedContextEngineMethodName; | |
| if (!fallback || !getFallbackEngine) { | |
| const invoke = (params: Record<string, unknown>) => | |
| method.call(engine, projectContextEngineHostParams(engine, methodName, params)); | |
| return inheritCompactionWatchdogOwnership(property, method, invoke); | |
| } | |
| const invokeFallback = async (methodParams: Record<string, unknown>) => { | |
| contextEngineAbortSignal(methodParams); | |
| return await invokeFallbackContextEngineMethod({ | |
| getFallbackEngine, | |
| methodName, | |
| methodParams, | |
| }); | |
| }; | |
| if (getContextEngineQuarantine(metadata.engineId)) { | |
| return methodName === "compact" | |
| ? markRuntimeCompactionDelegate(invokeFallback as ContextEngine["compact"]) // SAFETY: compact keeps this parameter contract. | |
| : invokeFallback; | |
| } | |
| const invoke = async (methodParams: Record<string, unknown>) => { | |
| const abortSignal = contextEngineAbortSignal(methodParams); | |
| if (getContextEngineQuarantine(metadata.engineId)) { | |
| // Runtime failures downgrade future guarded calls for this process. | |
| return await invokeFallback(methodParams); | |
| } | |
| try { | |
| return await method.call( | |
| engine, | |
| projectContextEngineHostParams(engine, methodName, methodParams), | |
| ); | |
| } catch (error) { | |
| if (isContextEngineAbortRejection(error, abortSignal)) { | |
| // Abort is caller intent, not engine instability; never quarantine for it. | |
| throw error; | |
| } | |
| recordContextEngineQuarantine({ | |
| engineId: metadata.engineId, | |
| owner: metadata.owner, | |
| operation: methodName, | |
| error, | |
| defaultEngineId: fallback.defaultEngineId, | |
| }); | |
| if (methodName === "compact" || methodName === "prepareSubagentSpawn") { | |
| throw error; | |
| } | |
| return await invokeFallback(methodParams).catch(() => { | |
| throw error; | |
| }); | |
| } | |
| }; | |
| return inheritCompactionWatchdogOwnership(property, method, invoke); | |
| }, | |
| }, | |
| ); | |
| resolvedEngineMetadata.set(wrapped, { | |
| ...metadata, | |
| sourceEngine: | |
| metadata.sourceEngine ?? resolvedEngineMetadata.get(engine)?.sourceEngine ?? engine, | |
| }); | |
| return wrapped; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Registry (module-level singleton) | |
| // --------------------------------------------------------------------------- | |
| const CONTEXT_ENGINE_REGISTRY_STATE = Symbol.for("openclaw.contextEngineRegistryState"); | |
| const CORE_CONTEXT_ENGINE_OWNER = "core"; | |
| type ContextEngineRuntimeQuarantine = { | |
| engineId: string; | |
| owner?: string; | |
| operation: string; | |
| reason: string; | |
| failedAt: Date; | |
| }; | |
| type ContextEngineRegistryState = { | |
| quarantinedEngines: Map<string, ContextEngineRuntimeQuarantine>; | |
| }; | |
| // Keep context-engine registrations process-global so duplicated dist chunks | |
| // still share one registry map at runtime. | |
| const contextEngineRegistryState = resolveGlobalSingleton<ContextEngineRegistryState>( | |
| CONTEXT_ENGINE_REGISTRY_STATE, | |
| () => ({ | |
| quarantinedEngines: new Map(), | |
| }), | |
| ); | |
| const getContextEngines = () => requireActivePluginRegistry().contextEngines; | |
| 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; | |
| } | |
| function recordContextEngineQuarantine(params: { | |
| engineId: string; | |
| owner?: string; | |
| operation: string; | |
| error: unknown; | |
| defaultEngineId: string; | |
| }): ContextEngineRuntimeQuarantine { | |
| const existing = contextEngineRegistryState.quarantinedEngines.get(params.engineId); | |
| if (existing) { | |
| // First failure wins so logs and diagnostics point at the root cause, not follow-on fallback use. | |
| return existing; | |
| } | |
| const quarantine: ContextEngineRuntimeQuarantine = { | |
| engineId: params.engineId, | |
| operation: params.operation, | |
| reason: params.error instanceof Error ? params.error.message : String(params.error), | |
| failedAt: new Date(), | |
| ...(params.owner ? { owner: params.owner } : {}), | |
| }; | |
| contextEngineRegistryState.quarantinedEngines.set(params.engineId, quarantine); | |
| try { | |
| recordPersistedContextEngineQuarantine(quarantine); | |
| } catch { | |
| // Quarantine behavior must not depend on the best-effort health mirror. | |
| } | |
| const ownerSuffix = params.owner ? ` owner=${sanitizeForLog(params.owner)}` : ""; | |
| console.error( | |
| `[context-engine] Context engine "${sanitizeForLog(params.engineId)}"${ownerSuffix} failed during ${sanitizeForLog(params.operation)}: ` + | |
| `${sanitizeForLog(quarantine.reason)}; quarantining it for this process and falling back to default engine "${params.defaultEngineId}".`, | |
| ); | |
| return quarantine; | |
| } | |
| function getContextEngineQuarantine(engineId: string): ContextEngineRuntimeQuarantine | undefined { | |
| return contextEngineRegistryState.quarantinedEngines.get(engineId); | |
| } | |
| export function listContextEngineQuarantines(): ContextEngineRuntimeQuarantine[] { | |
| const quarantines = Array.from( | |
| contextEngineRegistryState.quarantinedEngines.values(), | |
| ({ failedAt, ...quarantine }) => ({ ...quarantine, failedAt: new Date(failedAt) }), | |
| ); | |
| const seenEngineIds = new Set(quarantines.map((entry) => entry.engineId)); | |
| return quarantines.concat( | |
| listPersistedContextEngineQuarantines().filter(({ engineId }) => !seenEngineIds.has(engineId)), | |
| ); | |
| } | |
| function clearContextEngineRuntimeQuarantine(engineId: string): void { | |
| contextEngineRegistryState.quarantinedEngines.delete(engineId); | |
| clearPersistedContextEngineQuarantineForProcess(engineId, process.pid); | |
| } | |
| /** | |
| * Register a context engine implementation under an explicit trusted owner. | |
| */ | |
| export function registerContextEngineForOwner( | |
| id: string, | |
| factory: ContextEngineFactory, | |
| owner: string, | |
| opts?: RegisterContextEngineForOwnerOptions, | |
| ): ContextEngineRegistrationResult { | |
| const targetRegistry = requireActivePluginRegistry(); | |
| const result = registerContextEngineInRegistry(targetRegistry, id, factory, owner, opts); | |
| if ( | |
| result.ok && | |
| (opts?.lifecycle ?? "runtime") === "runtime" && | |
| getActivePluginRegistry() === targetRegistry | |
| ) { | |
| clearContextEngineRuntimeQuarantine(id); | |
| } | |
| return result; | |
| } | |
| /** Registers an engine in a registry value while that value is being assembled. */ | |
| export function registerContextEngineInRegistry( | |
| pluginRegistry: PluginRegistry, | |
| id: string, | |
| factory: ContextEngineFactory, | |
| owner: string, | |
| opts?: RegisterContextEngineForOwnerOptions, | |
| ): ContextEngineRegistrationResult { | |
| const normalizedOwner = requireContextEngineOwner(owner); | |
| const lifecycle = opts?.lifecycle ?? "runtime"; | |
| const registry = pluginRegistry.contextEngines; | |
| const existing = registry.get(id); | |
| if ( | |
| id === defaultSlotIdForKey("contextEngine") && | |
| normalizedOwner !== CORE_CONTEXT_ENGINE_OWNER | |
| ) { | |
| // The default fallback id is core-owned; plugins can select other ids through slots. | |
| return { ok: false, existingOwner: CORE_CONTEXT_ENGINE_OWNER }; | |
| } | |
| if (existing && existing.owner !== normalizedOwner) { | |
| return { ok: false, existingOwner: existing.owner }; | |
| } | |
| if (existing?.lifecycle === "runtime" && lifecycle === "readOnlyDiscovery") { | |
| // Read-only discovery may re-run after live activation. It can collect metadata, but it must | |
| // not replace the runtime-safe factory with a closure that captured a read-only plugin mode. | |
| return { ok: true }; | |
| } | |
| if (existing && opts?.allowSameOwnerRefresh !== true) { | |
| return { ok: false, existingOwner: existing.owner }; | |
| } | |
| const registration = { factory, owner: normalizedOwner, lifecycle }; | |
| recordContextEngineRegistrationSource(registration, pluginRegistry); | |
| registry.set(id, registration); | |
| return { ok: true }; | |
| } | |
| export { adoptRuntimeContextEngineRegistrations } from "./registry-adoption.js"; | |
| /** Clear runtime quarantine only after a complete builder-local registry becomes active. */ | |
| export function activateContextEngineRegistrations(pluginRegistry: PluginRegistry): void { | |
| for (const [id, registration] of pluginRegistry.contextEngines) { | |
| if (registration.lifecycle === "runtime") { | |
| clearContextEngineRuntimeQuarantine(id); | |
| } | |
| } | |
| } | |
| /** Returns registration metadata so callers can distinguish discovery snapshots from runtime entries. */ | |
| export function getContextEngineRegistration(id: string): ContextEngineRegistration | undefined { | |
| return getContextEngines().get(id); | |
| } | |
| const listContextEngineIds = () => [...getContextEngines().keys()].toSorted(); | |
| /** | |
| * Return the trusted plugin id that registered a resolved context engine. | |
| * Downgraded engines intentionally report no plugin owner. | |
| */ | |
| export function resolveContextEngineOwnerPluginId( | |
| engine: ContextEngine | undefined | null, | |
| ): string | undefined { | |
| const metadata = engine ? resolvedEngineMetadata.get(engine) : undefined; | |
| // Downgraded work belongs to its core-owned fallback, never the disabled plugin. | |
| const owner = | |
| metadata && !getContextEngineQuarantine(metadata.engineId) ? metadata.owner : undefined; | |
| return owner ? pluginIdFromContextEngineOwner(owner) : undefined; | |
| } | |
| export const hasSameContextEngineInstance = (left: ContextEngine, right: ContextEngine): boolean => | |
| (resolvedEngineMetadata.get(left)?.sourceEngine ?? left) === | |
| (resolvedEngineMetadata.get(right)?.sourceEngine ?? right); | |
| const CONTEXT_ENGINE_FALLBACK_RESULTS = { | |
| bootstrap: { bootstrapped: false, reason: "context engine downgraded to legacy" }, | |
| maintain: { | |
| changed: false, | |
| bytesFreed: 0, | |
| rewrittenEntries: 0, | |
| reason: "context engine downgraded to legacy", | |
| }, | |
| ingest: { ingested: false }, | |
| ingestBatch: { ingestedCount: 0 }, | |
| } as const satisfies { | |
| bootstrap: BootstrapResult; | |
| maintain: ContextEngineMaintenanceResult; | |
| ingest: IngestResult; | |
| ingestBatch: IngestBatchResult; | |
| }; | |
| export { isContextEngineAbortRejection }; | |
| async function invokeFallbackContextEngineMethod(params: { | |
| getFallbackEngine: () => Promise<ContextEngine>; | |
| methodName: GuardedContextEngineMethodName; | |
| methodParams: unknown; | |
| }): Promise<unknown> { | |
| const fallbackEngine = await params.getFallbackEngine(); | |
| const fallbackMethod = fallbackEngine[params.methodName] as | |
| | ((methodParams: unknown) => unknown) | |
| | undefined; | |
| if (typeof fallbackMethod === "function") { | |
| return await fallbackMethod.call(fallbackEngine, params.methodParams); | |
| } | |
| if (params.methodName === "assemble" || params.methodName === "compact") { | |
| throw new Error(`No legacy fallback result for ${params.methodName}`); | |
| } | |
| const fallbackResult = | |
| CONTEXT_ENGINE_FALLBACK_RESULTS[ | |
| params.methodName as keyof typeof CONTEXT_ENGINE_FALLBACK_RESULTS | |
| ]; | |
| return fallbackResult ? { ...fallbackResult } : undefined; | |
| } | |
| // --------------------------------------------------------------------------- | |
| // Resolution | |
| // --------------------------------------------------------------------------- | |
| /** | |
| * Options for {@link resolveContextEngine}. | |
| */ | |
| export type ResolveContextEngineOptions = { | |
| agentDir?: string; | |
| workspaceDir?: string; | |
| onCleanupFailure?: () => void; | |
| }; | |
| export type ResolvedContextEngineRef = Readonly<{ | |
| engine: ContextEngine; | |
| registeredId: string; | |
| ownerPluginId?: string; | |
| }>; | |
| export type LogicalTurnContextEngineResolution = { | |
| configured: ResolvedContextEngineRef; | |
| configuredId: string; | |
| configuredFailure?: string; | |
| fallback: ResolvedContextEngineRef; | |
| sourceResources?: ReadonlyMap<ContextEngine, readonly ContextEngineFactoryResources[]>; | |
| }; | |
| function resolvedContextEngineRef(params: { | |
| engine: ContextEngine; | |
| registeredId: string; | |
| owner: string; | |
| }): ResolvedContextEngineRef { | |
| const pluginId = pluginIdFromContextEngineOwner(params.owner); | |
| return Object.freeze({ | |
| engine: params.engine, | |
| registeredId: params.registeredId, | |
| ...(pluginId ? { ownerPluginId: pluginId } : {}), | |
| }); | |
| } | |
| async function createOwnedContextEngine( | |
| engineId: string, | |
| entry: ContextEngineRegistration, | |
| factoryCtx: ContextEngineFactoryContext, | |
| options: { | |
| defaultEngineId?: string; | |
| onValidation?: () => void; | |
| contractErrorPrefix?: string; | |
| source?: ContextEngineFactoryResources; | |
| ownsSource?: boolean; | |
| } = {}, | |
| ): Promise<ContextEngine> { | |
| let engine: ContextEngine | undefined; | |
| try { | |
| engine = await entry.factory(factoryCtx); | |
| options.onValidation?.(); | |
| const contractError = describeResolvedContextEngineContractError(engineId, engine); | |
| if (contractError) { | |
| throw new Error(`${options.contractErrorPrefix ?? ""}${contractError}`); | |
| } | |
| return wrapResolvedContextEngine(engine, { | |
| sourceEngine: resolvedEngineMetadata.get(engine)?.sourceEngine ?? engine, | |
| source: options.source, | |
| ownsSource: options.ownsSource, | |
| engineId, | |
| owner: entry.owner, | |
| factory: entry.factory, | |
| defaultEngineId: options.defaultEngineId, | |
| factoryCtx, | |
| }); | |
| } catch (error) { | |
| const dispose = () => engine?.dispose?.(); | |
| await Promise.resolve() | |
| .then(() => | |
| options.source | |
| ? options.source.runCleanup(dispose) | |
| : runPluginCleanup(entry.factory, dispose), | |
| ) | |
| .catch(() => undefined); | |
| throw error; | |
| } | |
| } | |
| async function resolveRawContextEngineRef( | |
| engineId: string, | |
| factoryCtx: ContextEngineFactoryContext, | |
| entry: ContextEngineRegistration | undefined, | |
| source: ContextEngineFactoryResources | undefined, | |
| ): Promise<ResolvedContextEngineRef> { | |
| if (!entry) { | |
| throw new Error( | |
| `Context engine "${engineId}" is not registered. ` + | |
| `Available engines: ${listContextEngineIds().join(", ") || "(none)"}`, | |
| ); | |
| } | |
| return resolvedContextEngineRef({ | |
| engine: await createOwnedContextEngine(engineId, entry, factoryCtx, { source }), | |
| registeredId: engineId, | |
| owner: entry.owner, | |
| }); | |
| } | |
| /** | |
| * Resolve fresh engines for one logical turn without consulting or mutating | |
| * process quarantine. A failed configured engine is retried by the next turn. | |
| */ | |
| export async function resolveLogicalTurnContextEngines( | |
| config?: OpenClawConfig, | |
| options?: ResolveContextEngineOptions, | |
| ): Promise<LogicalTurnContextEngineResolution> { | |
| return await runContextEngineFactoryResolution(async (abandon) => { | |
| const defaultEngineId = defaultSlotIdForKey("contextEngine"); | |
| const configuredEngineId = resolveEffectiveContextEngineId(config, getContextEngines()); | |
| const factoryCtx: ContextEngineFactoryContext = { | |
| config, | |
| agentDir: options?.agentDir, | |
| workspaceDir: options?.workspaceDir, | |
| }; | |
| const registry = requireActivePluginRegistry(); | |
| const entries = registry.contextEngines; | |
| const fallbackEntry = entries.get(defaultEngineId); | |
| const configuredEntry = entries.get(configuredEngineId); | |
| const sources = retainLogicalTurnContextEngineSources( | |
| registry, | |
| fallbackEntry, | |
| configuredEngineId === defaultEngineId ? undefined : configuredEntry, | |
| abandon, | |
| ); | |
| const sourceResources = new Map<ContextEngine, ContextEngineFactoryResources[]>(); | |
| let fallback: ResolvedContextEngineRef; | |
| try { | |
| fallback = await resolveContextEngineFactory(sources.fallback, sourceResources, () => | |
| resolveRawContextEngineRef(defaultEngineId, factoryCtx, fallbackEntry, sources.fallback), | |
| ); | |
| } catch (error) { | |
| abandon(sources.fallback); | |
| abandon(sources.configured); | |
| throw error; | |
| } | |
| if (configuredEngineId === defaultEngineId) { | |
| return { configured: fallback, configuredId: configuredEngineId, fallback, sourceResources }; | |
| } | |
| if (!configuredEntry || configuredEntry.lifecycle === "readOnlyDiscovery") { | |
| return { | |
| configured: fallback, | |
| configuredId: configuredEngineId, | |
| configuredFailure: !configuredEntry | |
| ? `context engine "${configuredEngineId}" is not registered` | |
| : `context engine "${configuredEngineId}" is available for discovery only`, | |
| fallback, | |
| sourceResources, | |
| }; | |
| } | |
| try { | |
| if (sources.configuredFailure) { | |
| throw sources.configuredFailure.error; | |
| } | |
| const configured = await resolveContextEngineFactory( | |
| sources.configured, | |
| sourceResources, | |
| () => | |
| resolveRawContextEngineRef( | |
| configuredEngineId, | |
| factoryCtx, | |
| configuredEntry, | |
| sources.configured, | |
| ), | |
| ); | |
| return { configured, configuredId: configuredEngineId, fallback, sourceResources }; | |
| } catch (error) { | |
| abandon(sources.configured); | |
| return { | |
| configured: fallback, | |
| configuredId: configuredEngineId, | |
| configuredFailure: error instanceof Error ? error.message : String(error), | |
| fallback, | |
| sourceResources, | |
| }; | |
| } | |
| }, options?.onCleanupFailure); | |
| } | |
| /** | |
| * Resolve which ContextEngine to use based on plugin slot configuration. | |
| * | |
| * Resolution order: | |
| * 1. `config.plugins.slots.contextEngine` when its plugin policy permits it | |
| * 2. Default slot value ("legacy") | |
| * | |
| * When `config` is provided it is forwarded to the factory as part of a | |
| * {@link ContextEngineFactoryContext}. Additional runtime paths can be | |
| * supplied via `options`. Existing no-arg factories continue to work | |
| * because JavaScript permits extra arguments at call sites. | |
| * | |
| * Non-default engines that fail (unregistered, factory throw, or contract | |
| * violation) are logged and silently replaced by the default engine. | |
| * Host admission/resource failures and owner cancellation propagate without quarantine. | |
| * Default-engine failures also propagate. | |
| */ | |
| export async function resolveContextEngine( | |
| config?: OpenClawConfig, | |
| options?: ResolveContextEngineOptions, | |
| ): Promise<ContextEngine> { | |
| const defaultEngineId = defaultSlotIdForKey("contextEngine"); | |
| const engineId = resolveEffectiveContextEngineId(config, getContextEngines()); | |
| const isDefaultEngine = engineId === defaultEngineId; | |
| const factoryCtx: ContextEngineFactoryContext = { | |
| config, | |
| agentDir: options?.agentDir, | |
| workspaceDir: options?.workspaceDir, | |
| }; | |
| const quarantine = !isDefaultEngine ? getContextEngineQuarantine(engineId) : undefined; | |
| if (quarantine) { | |
| // Previously failed custom engines stay downgraded until explicit quarantine clear/restart. | |
| return resolveDefaultContextEngine(defaultEngineId, factoryCtx); | |
| } | |
| const entry = getContextEngines().get(engineId); | |
| if (!entry) { | |
| if (isDefaultEngine) { | |
| throw new Error( | |
| `Context engine "${engineId}" is not registered. ` + | |
| `Available engines: ${listContextEngineIds().join(", ") || "(none)"}`, | |
| ); | |
| } | |
| recordContextEngineQuarantine({ | |
| engineId, | |
| operation: "resolve", | |
| error: "not registered", | |
| defaultEngineId, | |
| }); | |
| return resolveDefaultContextEngine(defaultEngineId, factoryCtx); | |
| } | |
| if (!isDefaultEngine && entry.lifecycle === "readOnlyDiscovery") { | |
| console.warn( | |
| `[context-engine] Context engine "${engineId}" owner=${entry.owner} is registered for read-only discovery only; falling back to default engine "${defaultEngineId}" without quarantine until runtime activation registers it.`, | |
| ); | |
| return resolveDefaultContextEngine(defaultEngineId, factoryCtx); | |
| } | |
| const abortSignal = getAsyncWorkSignal(); | |
| let operation: "factory" | "contract-validation" | undefined; | |
| try { | |
| return await createContextEngineWithResources( | |
| requireActivePluginRegistry(), | |
| entry, | |
| (source) => { | |
| // Admission and source retention belong to the host, not the plugin factory. | |
| operation = "factory"; | |
| return createOwnedContextEngine(engineId, entry, factoryCtx, { | |
| source, | |
| ownsSource: true, | |
| defaultEngineId, | |
| onValidation: () => { | |
| operation = "contract-validation"; | |
| }, | |
| }); | |
| }, | |
| ); | |
| } catch (error) { | |
| if (isDefaultEngine || !operation || isContextEngineAbortRejection(error, abortSignal)) { | |
| throw error; | |
| } | |
| recordContextEngineQuarantine({ | |
| engineId, | |
| owner: entry.owner, | |
| operation, | |
| error, | |
| defaultEngineId, | |
| }); | |
| return resolveDefaultContextEngine(defaultEngineId, factoryCtx); | |
| } | |
| } | |
| /** Default-engine failures propagate; they cannot select another fallback. */ | |
| async function resolveDefaultContextEngine( | |
| defaultEngineId: string, | |
| factoryCtx: ContextEngineFactoryContext, | |
| ): Promise<ContextEngine> { | |
| const defaultEntry = getContextEngines().get(defaultEngineId); | |
| if (!defaultEntry) { | |
| throw new Error( | |
| `[context-engine] fallback failed: default engine "${defaultEngineId}" is not registered. ` + | |
| `Available engines: ${listContextEngineIds().join(", ") || "(none)"}`, | |
| ); | |
| } | |
| return await createContextEngineWithResources( | |
| requireActivePluginRegistry(), | |
| defaultEntry, | |
| (source) => | |
| createOwnedContextEngine(defaultEngineId, defaultEntry, factoryCtx, { | |
| source, | |
| ownsSource: true, | |
| contractErrorPrefix: "[context-engine] ", | |
| }), | |
| ); | |
| } | |