UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

478 lines (477 loc) • 22 kB
import { a as resolveGlobalSingleton } from "./global-singleton-0m-JziJW.mjs"; import { c as getPluginCacheRetirementSignal, d as getPluginMetadataSnapshotCache } from "./plugin-cache-BABafcXD.mjs"; import { i as resolveRuntimeWorkerUrl } from "./runtime-worker-url-DoBSDhBS.mjs"; import { r as resolveStateDir } from "./state-dir-BHJORNly.mjs"; import { t as formatErrorMessage } from "./errors-O33D01XW.mjs"; import { t as createSubsystemLogger } from "./subsystem-I-uqdx4f.mjs"; import { n as runtimeProcessEntrypoints } from "./runtime-process-entrypoints-Dgz2IR4T.mjs"; import { r as WorkerTaskError } from "./src-WCJCjrey.mjs"; import { t as WorkerTaskPool } from "./worker-task-pool-iI3pfrsK.mjs"; import { n as isDeeplyFrozenPlainData } from "./immutable-data-MyNs7ITg.mjs"; import { c as getConfigResolutionFacts, m as serializeConfigResolutionFacts } from "./resolution-facts-HH1cnmIY.mjs"; import { n as createPluginSourceCaptureRoot } from "./plugin-source-capture-directory-suikpgfR.mjs"; import { a as resolveInstalledManifestRegistryIndexFingerprint } from "./plugin-control-plane-context-k1PmbRpn.mjs"; import { r as overlayPluginNativeAdmissions } from "./plugin-native-admission-state-D3dxXEX2.mjs"; import { n as projectConfigOntoRuntimeSourceSnapshot } from "./runtime-source-projection-rvQFgz1Z.mjs"; import { n as withPluginRuntimeGenerationScope } from "./generation-scope-tH9UXyLm.mjs"; import { t as captureRemoteModelCatalogSnapshot } from "./remote-overlay-C3A-3o4_.mjs"; import { _ as cloneAuthProfileStore } from "./oauth-shared-CFtD6sCU.mjs"; import { u as setPreparedModelFullCatalogAuth } from "./prepared-model-runtime-auth-Dn7HF9Vu.mjs"; import { i as createPreparedModelCatalogPreparationTimeout, r as PreparedModelCatalogGenerationMismatchError, t as PreparedModelCatalogAdmissionStalledError } from "./prepared-model-catalog.errors-CPinmjLF.mjs"; import { o as captureProviderSyntheticAuthFacts } from "./provider-runtime-D-Xv0Y4z.mjs"; import { t as listManifestSyntheticAuthProviderRefs } from "./synthetic-auth.runtime.js"; import { r as PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors-5KDV2tDJ.mjs"; import { o as registerPreparedModelRuntimeClose } from "./prepared-model-runtime.lifecycle-ADHVLWEn.mjs"; import { a as markPreparedModelCatalogFull } from "./prepared-model-runtime.full-catalog-DwiFZW5E.mjs"; import { n as captureClawInstallSchemaVersionFacts } from "./provenance-runtime-read-DLNIVZio.mjs"; import { l as scopeSyntheticAuthProviderRefs, n as fingerprintPreparedRuntimeFacts, s as listRegistrySyntheticAuthProviderRefs } from "./prepared-model-runtime.facts-COH422V0.mjs"; const CATALOG_WORKER_HEAP_LIMIT_MB = 512; /** * Without crash restart, the pool closes itself when its worker fails, exits or times out, even * while no task is waiting. It reports its first close so the owner can tell that from its own. */ var CatalogWorkerTaskPool = class extends WorkerTaskPool { constructor(env, validateResult, assertCurrent, onClose) { super({ workerUrl: resolveRuntimeWorkerUrl(runtimeProcessEntrypoints.preparedModelCatalog), workerOptions: { resourceLimits: { maxOldGenerationSizeMb: CATALOG_WORKER_HEAP_LIMIT_MB, maxYoungGenerationSizeMb: 32 } }, maxWorkers: 1, idleTimeoutMs: 0, restartOnError: false, prepareWorker: () => { assertCurrent?.(); const capture = createPluginSourceCaptureRoot(resolveStateDir(env), "openclaw-model-catalog-"); return { releaseResources: capture.release, options: { workerData: { sourceCaptureDirectory: capture.directory, sourceCaptureManagedRoot: capture.managedRoot }, env } }; }, validateResult }); this.onClose = onClose; } close(error) { const first = !this.isClosed; const closed = super.close(error); if (first) this.onClose?.(error); return closed; } }; //#endregion //#region src/agents/prepared-model-catalog-worker.ts /** Runs complete model-catalog discovery outside the Gateway event loop. */ const PREPARED_MODEL_CATALOG_WORKER_TIMEOUT_MS = 18e4; const log = createSubsystemLogger("agents/prepared-model-runtime"); const gatewayCatalog = resolveGlobalSingleton(Symbol.for("openclaw.gatewayModelCatalogPool"), () => ({})); function getPreparedModelCatalogWorkerPoolSnapshot() { return { ...gatewayCatalog.current?.pool.getSnapshot() ?? { maxWorkers: 1, workers: 0, workersCreated: 0, activeTasks: 0, pendingTasks: 0 }, workerFailures: gatewayCatalog.workerFailures ?? 0 }; } async function getGatewayCatalogPool(input, metadata, environmentFingerprint) { const cache = getPluginMetadataSnapshotCache(metadata); getPluginCacheRetirementSignal(cache).throwIfAborted(); if (gatewayCatalog.rotating) { await gatewayCatalog.rotating; return getGatewayCatalogPool(input, metadata, environmentFingerprint); } if (gatewayCatalog.current?.recovery) { await gatewayCatalog.current.recovery; return getGatewayCatalogPool(input, metadata, environmentFingerprint); } if (gatewayCatalog.current?.cache === cache) { if (gatewayCatalog.current.envFingerprint === environmentFingerprint) return gatewayCatalog.current; if ([...gatewayCatalog.current.borrowers].some((borrower) => borrower.isCurrent())) throw new Error("Gateway catalog environment changed without retiring its plugin generation"); } if (gatewayCatalog.current && [...gatewayCatalog.current.borrowers].some((borrower) => borrower.isCurrent())) throw new Error("Gateway catalog source generation changed before its previous owners retired"); gatewayCatalog.rotating = (async () => { await gatewayCatalog.current?.close(); const signal = getPluginCacheRetirementSignal(cache); signal.throwIfAborted(); let admissionStalled = false; const current = { cache, envFingerprint: environmentFingerprint, borrowers: /* @__PURE__ */ new Set(), recover: (error) => admissionStalled ? Promise.resolve() : current.recovery ??= (async () => { const borrowers = [...current.borrowers]; if (!signal.aborted) for (const borrower of borrowers) borrower.notifyRecovery(error); const stopping = borrowers.map((borrower) => borrower.stop(error)); await current.close(error); await Promise.all(stopping); if (gatewayCatalog.current === current) gatewayCatalog.current = void 0; const { recoverPreparedModelRuntimeCatalogWorker } = await import("./prepared-model-runtime-Dac6Df6C.mjs"); await recoverPreparedModelRuntimeCatalogWorker(borrowers); })(), close: async (error) => { current.closing = true; signal.removeEventListener("abort", retire); await current.pool.close(error); current.validate = void 0; release(); }, validate: void 0, pool: new CatalogWorkerTaskPool(input.input.env, (result) => { const validate = current.validate; current.validate = void 0; validate?.(result); }, () => signal.throwIfAborted(), (error) => { if (!current.closing && !signal.aborted) { admissionStalled = error instanceof PreparedModelCatalogAdmissionStalledError; gatewayCatalog.workerFailures = (gatewayCatalog.workerFailures ?? 0) + 1; log.warn(`model catalog worker failed; ${admissionStalled ? "native admission will not be retried automatically" : `${[...current.borrowers].filter((borrower) => borrower.isCurrent()).length} agent catalog(s) will be republished on a new worker`} (failure ${gatewayCatalog.workerFailures} since start): ${formatErrorMessage(error)}`); } }) }; const retire = () => { current.close(signal.reason).catch((error) => { process.emitWarning(`Gateway catalog worker failed to retire: ${String(error)}`); }); }; const release = registerPreparedModelRuntimeClose(async (error) => { await current.close(error); if (gatewayCatalog.current === current) gatewayCatalog.current = void 0; }); signal.addEventListener("abort", retire, { once: true }); gatewayCatalog.current = current; })(); try { await gatewayCatalog.rotating; } finally { gatewayCatalog.rotating = void 0; } return getGatewayCatalogPool(input, metadata, environmentFingerprint); } function fingerprintPreparedModelWorkerRequest(input, request) { return fingerprintPreparedRuntimeFacts([input.generationFingerprint, request]); } function fingerprintPreparedModelCatalogPlugins(snapshot) { return fingerprintPreparedRuntimeFacts({ config: snapshot.configFingerprint ?? null, index: resolveInstalledManifestRegistryIndexFingerprint(snapshot.index), pluginIds: snapshot.pluginIds ?? null, policy: snapshot.policyHash, workspaceDir: snapshot.workspaceDir ?? null }); } const immutableGenerationConfigFingerprints = /* @__PURE__ */ new WeakMap(); function fingerprintPreparedModelCatalogConfig(config) { const immutable = isDeeplyFrozenPlainData(config); const cached = immutable ? immutableGenerationConfigFingerprints.get(config) : void 0; if (cached !== void 0) return cached; const fingerprint = fingerprintPreparedRuntimeFacts(config); if (immutable) immutableGenerationConfigFingerprints.set(config, fingerprint); return fingerprint; } function fingerprintPreparedModelCatalogGeneration(params) { return fingerprintPreparedRuntimeFacts({ remoteCatalogSource: params.remoteCatalog?.sourceUrl, remoteCatalogRevision: params.remoteCatalog?.revision, input: { ...params.input, config: fingerprintPreparedModelCatalogConfig(params.input.config) }, sourceConfigForSecrets: fingerprintPreparedModelCatalogConfig(params.sourceConfigForSecrets), configResolutionFacts: params.configResolutionFacts, sourceConfigResolutionFacts: params.sourceConfigResolutionFacts, authStore: params.authStore, providerIds: params.providerIds, catalogFacts: params.catalogFacts, preferBuiltPluginArtifacts: params.preferBuiltPluginArtifacts, pluginFingerprint: fingerprintPreparedModelCatalogPlugins(params.pluginMetadataSnapshot) }); } /** Registrations follow their loader context; agent credentials remain request-local. */ function fingerprintPreparedModelCatalogPluginContext(value) { return fingerprintPreparedRuntimeFacts({ remoteCatalogSource: value.remoteCatalog?.sourceUrl, remoteCatalogRevision: value.remoteCatalog?.revision, config: fingerprintPreparedModelCatalogConfig(value.input.config), sourceConfigForSecrets: fingerprintPreparedModelCatalogConfig(value.sourceConfigForSecrets), configResolutionFacts: value.configResolutionFacts, sourceConfigResolutionFacts: value.sourceConfigResolutionFacts, env: value.input.env, workspaceDir: value.pluginMetadataSnapshot.workspaceDir ?? value.input.workspaceDir, allowGatewaySubagentBinding: value.input.allowGatewaySubagentBinding === true, preferBuiltPluginArtifacts: value.preferBuiltPluginArtifacts, pluginFingerprint: fingerprintPreparedModelCatalogPlugins(value.pluginMetadataSnapshot) }); } function createPreparedModelCatalogWorkerInput(params) { const source = params.agentFacts.input; const input = { ...source.agentId ? { agentId: source.agentId } : {}, agentDir: source.agentDir, ...source.inheritedAuthDir ? { inheritedAuthDir: source.inheritedAuthDir } : {}, ...source.workspaceDir ? { workspaceDir: source.workspaceDir } : {}, ...source.readOnly ? { readOnly: true } : {}, env: { ...params.agentFacts.env }, ...source.allowGatewaySubagentBinding ? { allowGatewaySubagentBinding: true } : {}, config: source.config }; const sourceConfigForSecrets = projectConfigOntoRuntimeSourceSnapshot(source.config); const configResolutionFacts = serializeConfigResolutionFacts(source.config); const sourceConfigResolutionFacts = getConfigResolutionFacts(source.config) === getConfigResolutionFacts(sourceConfigForSecrets) ? configResolutionFacts : serializeConfigResolutionFacts(sourceConfigForSecrets); const { normalizePluginId: _normalizePluginId, ...pluginMetadataSnapshot } = params.pluginMetadataSnapshot; const cache = getPluginMetadataSnapshotCache(params.pluginMetadataSnapshot); const index = overlayPluginNativeAdmissions(pluginMetadataSnapshot.index, cache); const value = { remoteCatalog: captureRemoteModelCatalogSnapshot(), input, sourceConfigForSecrets, configResolutionFacts, sourceConfigResolutionFacts, authStore: cloneAuthProfileStore(params.agentFacts.authStore), providerIds: [...params.agentFacts.providerIds], catalogFacts: { configuredModelRefs: params.agentFacts.configuredModelRefs, configuredRuntimeModels: params.agentFacts.configuredRuntimeModels }, preferBuiltPluginArtifacts: params.preferBuiltPluginArtifacts === true, pluginMetadataSnapshot: { ...pluginMetadataSnapshot, index, registryIndex: pluginMetadataSnapshot.registryIndex === pluginMetadataSnapshot.index ? index : overlayPluginNativeAdmissions(pluginMetadataSnapshot.registryIndex, cache) } }; return { ...value, generationFingerprint: fingerprintPreparedModelCatalogGeneration(value) }; } function createPreparedModelCatalogWorker(params) { const workerInput = createPreparedModelCatalogWorkerInput(params); const metadataSnapshot = params.pluginMetadataSnapshot; const superseded = () => new PreparedModelRuntimePublicationSupersededError(`prepared model runtime catalog generation was superseded for ${workerInput.input.agentDir}`); let observingRetirement = false; let stoppedError; let releaseProcessLifetime; let expectedFingerprint; let pendingAuth; const captures = /* @__PURE__ */ new Map(); const tasks = /* @__PURE__ */ new Map(); const assertCurrent = () => { if (stoppedError) throw stoppedError; if (!params.isCurrent()) throw superseded(); }; const gatewayOwned = params.agentFacts.input.allowGatewaySubagentBinding === true && params.agentFacts.input.env === void 0; const environmentFingerprint = fingerprintPreparedRuntimeFacts(workerInput.input.env); let pool; let sharedOwner; const mismatch = (message) => new PreparedModelCatalogGenerationMismatchError(workerInput.input.agentDir, message.generationFingerprint, message.reconstructedFingerprint); const validate = (message) => { if (!gatewayOwned) assertCurrent(); if (message.status === "generation-mismatch") throw mismatch(message); if (message.status === "ok" && message.generationFingerprint !== expectedFingerprint) throw new Error("prepared model catalog worker returned a stale generation"); }; const stop = async (error) => { stoppedError ??= error; params.retirementSignal.removeEventListener("abort", retire); for (const controller of captures.keys()) controller.abort(stoppedError); await Promise.allSettled(captures.values()); if (gatewayOwned) await Promise.allSettled(tasks.keys()); else await pool?.close(stoppedError); sharedOwner?.borrowers.delete(borrower); releaseProcessLifetime?.(); releaseProcessLifetime = void 0; }; const retire = () => { queueMicrotask(() => { stop(superseded()).catch((error) => { process.emitWarning(`Prepared model catalog worker failed to retire: ${String(error)}`); }); }); }; const borrower = { agentDir: workerInput.input.agentDir, isCurrent: params.isCurrent, notifyRecovery: (error) => { if (stoppedError || !params.isCurrent()) return; for (const task of tasks.values()) task.onRecovery?.(error); }, stop }; const request = async (command, onRecovery) => { let message; let requestPool; let pending; const task = {}; const controller = new AbortController(); let progress; let expire = () => controller.abort(new WorkerTaskError("worker task timed out", "timeout")); const timeout = setTimeout(() => expire(), PREPARED_MODEL_CATALOG_WORKER_TIMEOUT_MS); try { assertCurrent(); releaseProcessLifetime ??= registerPreparedModelRuntimeClose(stop); if (!observingRetirement) { observingRetirement = true; params.retirementSignal.addEventListener("abort", retire, { once: true }); if (params.retirementSignal.aborted) retire(); } const { input } = workerInput; const providerScope = [...workerInput.providerIds, ...command.providerIds ?? []]; const manifestRefs = listManifestSyntheticAuthProviderRefs(metadataSnapshot.index); const capture = withPluginRuntimeGenerationScope({ metadataSnapshot, pluginRegistry: params.pluginRegistry }, () => captureProviderSyntheticAuthFacts({ config: input.config, env: input.env, workspaceDir: input.workspaceDir, providerRefs: command.kind === "catalog" && !command.providerIds ? [ ...manifestRefs, ...listRegistrySyntheticAuthProviderRefs(params.pluginRegistry), ...workerInput.providerIds ] : [...providerScope, ...scopeSyntheticAuthProviderRefs(manifestRefs, providerScope)], signal: controller.signal })); captures.set(controller, capture); let syntheticAuth; try { syntheticAuth = await capture; } finally { captures.delete(controller); } controller.signal.throwIfAborted(); const value = { ...command, syntheticAuth }; const shared = gatewayOwned ? await getGatewayCatalogPool(workerInput, metadataSnapshot, environmentFingerprint) : void 0; if (shared) { sharedOwner = shared; shared.borrowers.add(borrower); } requestPool = pool = shared?.pool ?? pool ?? new CatalogWorkerTaskPool(workerInput.input.env, validate); pending = requestPool.run(() => { assertCurrent(); expire = () => { const failure = createPreparedModelCatalogPreparationTimeout(progress?.active ? progress.pluginId : void 0, PREPARED_MODEL_CATALOG_WORKER_TIMEOUT_MS); borrower.notifyRecovery(failure); requestPool.close(failure).catch((error) => process.emitWarning(String(error))); }; timeout.refresh(); task.onRecovery = onRecovery; const workerRequest = { ...value, clawInstallSchemaVersions: captureClawInstallSchemaVersionFacts({ env: input.env }) }; expectedFingerprint = fingerprintPreparedModelWorkerRequest(workerInput, workerRequest); if (shared) shared.validate = validate; return { value: workerInput, request: workerRequest }; }, { signal: controller.signal, onNotification: (notification) => { const next = notification; if (!progress || next.completed > progress.completed) timeout.refresh(); progress = next; }, onRequest: async () => { clearTimeout(timeout); return { input: !stoppedError && params.isCurrent(), timeoutMs: PREPARED_MODEL_CATALOG_WORKER_TIMEOUT_MS }; } }); tasks.set(pending, task); message = await pending; assertCurrent(); } catch (error) { const failure = error instanceof Error ? error : new Error(String(error)); if (failure instanceof PreparedModelCatalogAdmissionStalledError) throw failure; if (failure instanceof WorkerTaskError && failure.code === "overloaded") throw failure; if (gatewayOwned && requestPool && !requestPool.isClosed && params.isCurrent()) { controller.abort(error); throw error; } if (gatewayOwned && sharedOwner && sharedOwner.pool.isClosed && !(failure instanceof PreparedModelRuntimePublicationSupersededError)) await sharedOwner.recover(failure).catch((recoveryError) => { process.emitWarning(`Gateway catalog recovery failed: ${String(recoveryError)}`); }); if (!gatewayOwned && failure instanceof PreparedModelCatalogGenerationMismatchError) { if (pool === requestPool) pool = void 0; await requestPool?.close(failure); throw failure; } controller.abort(error); await stop(failure); throw error; } finally { task.onRecovery = void 0; if (pending) tasks.delete(pending); clearTimeout(timeout); } if (message.status === "failed") throw new Error(message.error); if (message.status === "generation-mismatch") throw mismatch(message); return message; }; return { loadCatalog: async (providerIds, onRecovery) => { const message = await request({ kind: "catalog", ...providerIds ? { providerIds } : {} }, onRecovery); if (message.kind !== "catalog") throw new Error("prepared model catalog worker returned an auth refresh result"); const modelCatalog = markPreparedModelCatalogFull(message.snapshot); setPreparedModelFullCatalogAuth(modelCatalog, { authStore: message.authStore, authModes: message.authModes, credentials: message.credentials, providerAuthLabels: message.providerAuthLabels }); return { modelCatalog, configuredRuntimeModels: message.configuredRuntimeModels, runtimeModels: message.runtimeModels, providerExpiries: message.providerExpiries, hookRows: message.hookRows }; }, loadAuth: async ({ providerIds, profileIds }) => { const normalizedProviderIds = [...new Set(providerIds)].toSorted((left, right) => left.localeCompare(right)); const normalizedProfileIds = profileIds && [...new Set(profileIds)].toSorted((left, right) => left.localeCompare(right)); const key = JSON.stringify([normalizedProviderIds, normalizedProfileIds]); if (pendingAuth?.key === key) return pendingAuth.promise; const promise = request({ kind: "auth-refresh", providerIds: normalizedProviderIds, ...normalizedProfileIds ? { profileIds: normalizedProfileIds } : {} }).then((message) => { if (message.kind !== "auth-refresh") throw new Error("prepared model auth refresh worker returned a catalog result"); return { authStore: message.authStore, authModes: message.authModes, credentials: message.credentials }; }).finally(() => { if (pendingAuth?.promise === promise) pendingAuth = void 0; }); pendingAuth = { key, promise }; return promise; } }; } //#endregion export { fingerprintPreparedModelCatalogPluginContext as a, fingerprintPreparedModelCatalogGeneration as i, createPreparedModelCatalogWorker as n, fingerprintPreparedModelWorkerRequest as o, createPreparedModelCatalogWorkerInput as r, getPreparedModelCatalogWorkerPoolSnapshot as s, PREPARED_MODEL_CATALOG_WORKER_TIMEOUT_MS as t };