openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
532 lines (531 loc) • 22.3 kB
JavaScript
import { t as pruneMapToMaxSize } from "./map-size-BlrH_CnE.mjs";
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { t as formatErrorMessage } from "./errors-BvOkmQM5.mjs";
import { n as normalizeAgentId } from "./agent-id-D1-Q6vR4.mjs";
import "./session-key-DY3MOX4N.mjs";
import { n as assertAgentDatabaseAdmitted } from "./agent-database-admission-CRwW7FsI.mjs";
import { t as captureOpenClawStateReadContext } from "./openclaw-state-worker-context-BvvgY6-t.mjs";
import { d as getActivePluginRegistryVersion } from "./runtime-U4i1bMh7.mjs";
import { v as isUserModelAuthProfileId } from "./profile-usage-stats-DpmWbjlU.mjs";
import { a as getPreparedRuntimeAuthProfileStoreSnapshot } from "./store-C7nkCeF3.mjs";
import { P as captureUserProfileModelAccountLinksAuthority } from "./user-profiles-internal-B_G1rF82.mjs";
import { a as listUserProfileAuthLinks, o as listUserProfileAuthLinksAsync } from "./user-model-accounts-_80jPaa1.mjs";
import { l as getRuntimeAuthProfileStoreMetadataRevision } from "./runtime-snapshots-DU02_IZD.mjs";
import { o as getPreparedModelRuntimeAuthMaterializations } from "./prepared-model-runtime-auth-DKS2l7Wv.mjs";
import "./auth-profiles-DpzfW6rH.mjs";
import { n as PreparedModelRuntimePublicationSupersededError } from "./prepared-model-runtime.errors-UvN8VA9R.mjs";
import { n as withCurrentReadAuthority } from "./current-read-authority-CySCga_Z.mjs";
import { r as getPublishedPreparedModelCatalogOwnerSnapshot } from "./prepared-model-catalog-KgiwpXAj.mjs";
import { t as resolveSwarmConfig } from "./swarm-config-DoMZdfR2.mjs";
import { i as getSkillsSnapshotVersion } from "./refresh-state-CJGdcI_j.mjs";
import { a as generationFactsMatch, i as captureGenerationFacts, n as assertPreparedAgentCurrent, r as authStoresCurrent, t as ChatMetadataSnapshotUnavailableError } from "./chat-metadata-facts-BE_QAQy2.mjs";
import { a as readPreparedChatMetadata, c as prepareChatAccountSelection, n as prepareChatMetadataModelProjection, o as resolveSessionCatalogProfiles, r as prepareSessionAcpMeta, s as sessionProjectionKey, t as hasSessionCatalogContext } from "./chat-metadata-session-projection-CeMkdpON.mjs";
import { AsyncLocalStorage } from "node:async_hooks";
import { createHash } from "node:crypto";
//#region src/gateway/server-methods/chat-metadata-model-list.ts
/** Model-list variants are derived facts of the existing chat metadata generation. */
function createChatMetadataModelList(params) {
const agents = new Map(params.facts.agents.map((agent) => [agent.agentId, agent]));
for (const [key, request] of params.requested) if (!agents.has(request.agentId)) params.requested.delete(key);
const projections = /* @__PURE__ */ new Map();
const prepare = (request, authority) => {
const key = JSON.stringify([
request.agentId,
request.params,
request.includeManualSelection,
request.readScope?.sessionEntry
]);
const existing = projections.get(key);
if (existing) return existing.then((projection) => {
if (projection.isCurrent()) return projection;
if (projections.get(key) === existing) projections.delete(key);
return prepare(request, authority);
});
const facts = agents.get(request.agentId);
if (!facts?.owner.catalogOwner || !facts.owner.isCurrent()) return Promise.reject(new PreparedModelRuntimePublicationSupersededError("Model catalog changed while preparing this result. Retry the request."));
const { owner, modelCatalog, authStore, authModes } = facts;
const catalogOwner = facts.owner.catalogOwner;
const pending = import("./models-list-result-DNp2O-xL.mjs").then(({ prepareModelsListResult }) => prepareModelsListResult({
...request,
preparationAuthority: authority,
source: {
kind: "published",
getConfig: params.context.getRuntimeConfig,
owner: {
...owner,
catalogOwner,
agentId: request.agentId,
workspaceDir: catalogOwner.workspaceDir,
modelCatalog,
authStore,
authModes,
authMaterializations: getPreparedModelRuntimeAuthMaterializations(owner)
}
},
preloadedOnly: true
})).then((projection) => {
params.requested.set(key, {
...request,
params: { ...request.params }
});
pruneMapToMaxSize(params.requested, params.maxEntries);
return projection;
});
projections.set(key, pending);
pruneMapToMaxSize(projections, params.maxEntries);
pending.catch(() => {
if (projections.get(key) === pending) projections.delete(key);
});
return pending;
};
return {
prepare,
async read(request) {
const { requesterProfileId, readScope, ...shared } = request;
if (readScope) {
const entry = readScope.sessionEntry;
if (!readScope.sessionKey || isUserModelAuthProfileId(entry?.authProfileOverride?.trim() ?? "")) return;
return prepare({
...shared,
readScope: {
agentId: request.agentId,
sessionKey: readScope.sessionKey,
sessionEntry: entry && {
modelOverride: entry.modelOverride,
providerOverride: entry.providerOverride,
modelOverrideRouteResolution: entry.modelOverrideRouteResolution,
modelOverrideFallbackOriginProvider: entry.modelOverrideFallbackOriginProvider,
modelOverrideFallbackOriginModel: entry.modelOverrideFallbackOriginModel,
agentRuntimeOverride: entry.agentRuntimeOverride,
authProfileOverride: entry.authProfileOverride,
authProfileOverrideSource: entry.authProfileOverrideSource,
authProfileOverrideCompactionCount: entry.authProfileOverrideCompactionCount
}
}
}, readScope);
}
if (requesterProfileId && request.params.view !== "provider-config") {
const authority = captureUserProfileModelAccountLinksAuthority(captureOpenClawStateReadContext().admission, requesterProfileId);
const links = await listUserProfileAuthLinksAsync(requesterProfileId);
if (!authority()) return {
isCurrent: () => false,
read: () => ({ models: [] })
};
if (links.length > 0) return;
const projection = await prepare(shared);
return {
isCurrent: () => authority() && projection.isCurrent(),
read: () => ({
...projection.read(),
accountSelection: {
kind: "automatic",
label: "Automatic account selection"
}
})
};
}
return prepare(shared);
}
};
}
//#endregion
//#region src/gateway/server-methods/chat-metadata-runtime.ts
const CHAT_METADATA_CACHE_MAX_ENTRIES = 64;
function createGatewayChatMetadataRuntime(params) {
const deps = {
getConfig: params.getConfig,
getContext: params.getContext,
getPreparedOwner: getPublishedPreparedModelCatalogOwnerSnapshot,
getPreparedAuthStore: getPreparedRuntimeAuthProfileStoreSnapshot,
getAuthStoreRevision: getRuntimeAuthProfileStoreMetadataRevision,
getSkillsVersion: getSkillsSnapshotVersion,
getPluginRegistryVersion: getActivePluginRegistryVersion,
buildCommands: async ({ cfg, agentId }) => {
const { buildCommandsListResult } = await import("./commands-list-result-B4GUjLZy.mjs");
return buildCommandsListResult({
cfg,
agentId,
includeArgs: true,
scope: "text"
});
},
buildProjection: prepareChatMetadataModelProjection,
...params.deps
};
const requestedModelLists = /* @__PURE__ */ new Map();
let current;
let lastError;
let replacement;
let invalidationEpoch = 0;
let refreshVersion = 0;
let lastSettlement;
let lastNotifiedFacts;
const notifyChanged = (facts, catalogStatusChanged = false) => {
const previous = lastNotifiedFacts;
lastNotifiedFacts = facts;
params.onChanged?.({
modelCatalogChanged: catalogStatusChanged || !previous || !facts || !generationFactsMatch(previous, facts, "catalog"),
authChanged: !previous || !facts || !generationFactsMatch(previous, facts, "auth"),
...previous && facts && generationFactsMatch(previous, facts, "commands") ? { commandsChanged: false } : {}
});
};
let refreshTail = Promise.resolve();
let stoppedError;
const activeWork = /* @__PURE__ */ new Set();
const trackWork = (work) => {
activeWork.add(work);
const settled = () => activeWork.delete(work);
work.then(settled, settled);
return work;
};
const assertOpen = () => {
if (stoppedError) throw stoppedError;
};
let pending;
const projectAgent = async (generation, agent, sessionEntry, requesterProfileId, authority, useRequesterDefaults = false) => {
assertOpen();
authority?.assertCurrent?.();
assertPreparedAgentCurrent(agent);
const profiles = resolveSessionCatalogProfiles(sessionEntry, agent.owner.config, agent.agentId);
const neutral = !hasSessionCatalogContext(profiles);
const defaultProfileId = useRequesterDefaults && !profiles.preferredProfileId && requesterProfileId && listUserProfileAuthLinks(requesterProfileId).length > 0 ? requesterProfileId : void 0;
const projections = !profiles.preferredProfileId && defaultProfileId || isUserModelAuthProfileId(profiles.preferredProfileId ?? "") ? /* @__PURE__ */ new Map() : neutral ? generation.neutralProjectionByAgentId : generation.sessionProjectionByKey;
const key = neutral ? agent.agentId : sessionProjectionKey(agent.agentId, profiles);
const existing = projections.get(key);
if (existing) {
const prepared = existing.state === "ready" ? existing.projection : await existing.promise;
if (await withCurrentReadAuthority(authority, () => prepared.isCurrent())) return prepared;
if (projections.get(key) === existing) projections.delete(key);
return projectAgent(generation, agent, sessionEntry, requesterProfileId, authority, useRequesterDefaults);
}
const projection = deps.buildProjection({
context: deps.getContext(),
facts: agent,
requesterProfileId: defaultProfileId,
...authority,
...profiles
}).then((prepared) => withCurrentReadAuthority(authority, () => {
const preparedProjection = {
...prepared,
agent
};
if (generation.epoch === invalidationEpoch && projections.get(key) === entry) projections.set(key, {
state: "ready",
projection: preparedProjection
});
return preparedProjection;
})).catch((error) => {
if (projections.get(key) === entry) projections.delete(key);
throw error;
});
trackWork(projection);
const entry = {
state: "pending",
promise: projection
};
projections.set(key, entry);
pruneMapToMaxSize(projections, CHAT_METADATA_CACHE_MAX_ENTRIES);
return projection;
};
const prepareAgent = (generation, agentId) => {
const existing = generation.agentsById.get(agentId);
if (existing) return existing;
const agent = generation.facts.agents.find((candidate) => candidate.agentId === agentId);
if (!agent) return;
const preparing = trackWork((async () => {
let commands;
try {
commands = (await deps.buildCommands({
cfg: generation.facts.config,
agentId
})).commands;
} catch (error) {
params.log.warn(`chat metadata continuing without text commands for ${agentId}: ${formatErrorMessage(error)}`);
}
const swarmEnabled = resolveSwarmConfig(generation.facts.config, agentId).enabled;
return {
...agent,
...commands !== void 0 ? { commands } : {},
swarmEnabled,
revision: createHash("sha256").update(JSON.stringify({
commands,
swarmEnabled
})).digest("base64url")
};
})().catch((error) => {
if (generation.agentsById.get(agentId) === preparing) generation.agentsById.delete(agentId);
throw error;
}));
generation.agentsById.set(agentId, preparing);
pruneMapToMaxSize(generation.agentsById, CHAT_METADATA_CACHE_MAX_ENTRIES);
return preparing;
};
const runRefresh = async (version) => {
if (version !== refreshVersion) return;
assertOpen();
try {
const facts = captureGenerationFacts(deps);
if (current && generationFactsMatch(current.facts, facts)) return;
current = {
epoch: invalidationEpoch,
facts,
modelLists: createChatMetadataModelList({
facts,
context: deps.getContext(),
requested: requestedModelLists,
maxEntries: CHAT_METADATA_CACHE_MAX_ENTRIES
}),
agentsById: /* @__PURE__ */ new Map(),
neutralProjectionByAgentId: /* @__PURE__ */ new Map(),
sessionProjectionByKey: /* @__PURE__ */ new Map()
};
const { modelLists } = current;
await Promise.allSettled([...requestedModelLists.values()].map((request) => modelLists.prepare(request)));
} catch (error) {
if (version !== refreshVersion) return;
throw error;
}
};
const refresh = AsyncLocalStorage.bind((options = {}) => {
if (stoppedError) return Promise.reject(stoppedError);
let facts;
try {
facts = captureGenerationFacts(deps);
} catch (error) {
const refreshError = error instanceof Error ? error : new Error(formatErrorMessage(error));
fail(refreshError);
return Promise.reject(refreshError);
}
if (current && generationFactsMatch(current.facts, facts)) {
if (options.notifyIfUnchanged) notifyChanged(current.facts, true);
return Promise.resolve();
}
if (pending && generationFactsMatch(pending.facts, facts)) {
pending.notifyIfUnchanged ||= options.notifyIfUnchanged === true;
return pending.promise;
}
if (current || pending) invalidate(true);
const version = ++refreshVersion;
const promise = refreshTail.catch(() => {}).then(() => runRefresh(version));
refreshTail = promise;
const generationReady = createDeferredCore();
pending = {
facts,
promise,
generationReady,
notifyIfUnchanged: options.notifyIfUnchanged === true
};
promise.then(() => {
generationReady.resolve();
if (pending?.promise !== promise) return;
const notifyIfUnchanged = pending.notifyIfUnchanged;
pending = void 0;
if (current?.epoch !== invalidationEpoch) return;
lastError = void 0;
const committedReplacement = replacement;
replacement = void 0;
committedReplacement?.resolve();
if (lastSettlement !== current || notifyIfUnchanged) {
lastSettlement = current;
notifyChanged(current.facts, notifyIfUnchanged);
}
}, (error) => {
generationReady.resolve();
if (pending?.promise !== promise) return;
pending = void 0;
fail(error);
});
return promise;
});
const isCurrentGeneration = (generation) => current === generation && generation.epoch === invalidationEpoch && authStoresCurrent(generation.facts, deps);
const readCurrent = async (project) => {
for (;;) {
assertOpen();
const preparation = pending;
if (preparation) {
await preparation.generationReady.promise;
continue;
}
let generation = current;
const replacementPromise = replacement?.promise;
if (!generation && replacementPromise) {
await replacementPromise;
continue;
}
const retryUnavailableOwner = lastError instanceof ChatMetadataSnapshotUnavailableError;
if (!generation && retryUnavailableOwner) {
await refresh();
generation = current;
}
if (!generation) throw lastError ?? new ChatMetadataSnapshotUnavailableError();
if (!authStoresCurrent(generation.facts, deps)) {
await refresh();
continue;
}
try {
const readProjection = await project(generation);
if (isCurrentGeneration(generation) && readProjection.isCurrent()) return readProjection.read();
} catch (error) {
if (isCurrentGeneration(generation)) throw error;
}
}
};
const readModelsList = async (request) => {
assertAgentDatabaseAdmitted(request.agentId);
return readCurrent(async (generation) => {
return await generation.modelLists.read(request) ?? {
isCurrent: () => true,
read: () => void 0
};
});
};
const read = async (readParams) => {
assertAgentDatabaseAdmitted(readParams.agentId);
const { draftAccountSelection: draft, isCurrent } = readParams;
const assertCurrent = isCurrent ? () => {
if (!isCurrent()) throw new PreparedModelRuntimePublicationSupersededError("Chat metadata access changed while preparing its metadata. Retry the request.");
draft?.assertCurrent();
} : draft?.assertCurrent;
const authority = (readParams.withCurrent || readParams.assertCurrent || assertCurrent) && {
withCurrent: readParams.withCurrent,
assertCurrent: () => {
readParams.assertCurrent?.();
assertCurrent?.();
}
};
const sessionEntry = draft ? {
authProfileOverride: draft.authProfileId,
authProfileOverrideSource: "user"
} : readParams.sessionEntry;
return await readCurrent(async (generation) => {
const agentId = normalizeAgentId(readParams.agentId);
const agent = await prepareAgent(generation, agentId);
if (!agent) throw new ChatMetadataSnapshotUnavailableError(`prepared chat metadata is unavailable for agent "${agentId}"`);
await withCurrentReadAuthority(authority, () => {});
if (readParams.includeModels === false) {
assertPreparedAgentCurrent(agent);
return {
isCurrent: agent.owner.isCurrent,
read: () => {
draft?.assertCurrent();
return {
...readParams.ifRevision === agent.revision ? { unchanged: true } : { commands: agent.commands },
swarmEnabled: agent.swarmEnabled,
revision: agent.revision
};
}
};
}
const projection = await withCurrentReadAuthority(authority, () => projectAgent(generation, agent, sessionEntry, draft?.owner ?? readParams.requesterProfileId, authority, !readParams.sessionKey && !readParams.sessionEntry));
const readAccountSelection = await withCurrentReadAuthority(authority, () => prepareChatAccountSelection({
authStore: agent.authStore,
sessionEntry,
requesterProfileId: draft?.owner ?? readParams.requesterProfileId
}));
const acpMeta = await withCurrentReadAuthority(authority, () => prepareSessionAcpMeta({
...readParams,
sessionEntry
}, deps.getConfig()));
await withCurrentReadAuthority(authority, () => {});
return {
isCurrent: projection.isCurrent,
read: () => readPreparedChatMetadata(projection, {
...readParams,
sessionEntry,
requesterProfileId: draft?.owner ?? readParams.requesterProfileId
}, deps.getConfig(), acpMeta, readAccountSelection)
};
});
};
const readStartup = async (readParams) => {
assertAgentDatabaseAdmitted(readParams.agentId);
const profiles = resolveSessionCatalogProfiles(readParams.sessionEntry, deps.getConfig(), readParams.agentId);
const hasSessionContext = hasSessionCatalogContext(profiles);
const assemble = (neutral, session, acpMeta, readAccountSelection) => ({
...readParams.readPolicy === "ready" ? {} : { metadata: readPreparedChatMetadata(session, {
...readParams,
requesterProfileId: readParams.readRequesterProfileId?.()
}, deps.getConfig(), acpMeta, readAccountSelection) },
sessionModelCatalog: session.modelCatalog,
defaultModelCatalog: neutral.modelCatalog
});
const projectStartup = async (generation) => {
const agentId = normalizeAgentId(readParams.agentId);
const agent = await prepareAgent(generation, agentId);
if (!agent) throw new ChatMetadataSnapshotUnavailableError(`prepared chat startup projection is unavailable for agent "${agentId}"`);
const readNeutral = await projectAgent(generation, agent);
const readSession = hasSessionContext ? await projectAgent(generation, agent, readParams.sessionEntry, readParams.readRequesterProfileId?.()) : readNeutral;
const readAccountSelection = await prepareChatAccountSelection({
authStore: agent.authStore,
sessionEntry: readParams.sessionEntry,
readRequesterProfileId: readParams.readRequesterProfileId
});
const acpMeta = await prepareSessionAcpMeta(readParams, deps.getConfig());
return {
isCurrent: () => readNeutral.isCurrent() && readSession.isCurrent(),
read: () => assemble(readNeutral, readSession, acpMeta, readAccountSelection)
};
};
if (readParams.readPolicy !== "ready" && hasSessionContext) return readCurrent(projectStartup);
if (isUserModelAuthProfileId(profiles.preferredProfileId ?? "")) return;
const generation = current;
if (!generation || replacement || pending || !isCurrentGeneration(generation)) return;
const agentId = normalizeAgentId(readParams.agentId);
const neutral = generation.neutralProjectionByAgentId.get(agentId);
const session = hasSessionContext ? generation.sessionProjectionByKey.get(sessionProjectionKey(agentId, profiles)) : neutral;
if (neutral?.state !== "ready" || session?.state !== "ready" || !neutral.projection.isCurrent() || !session.projection.isCurrent()) return;
if (readParams.readPolicy === "ready") return assemble(neutral.projection, session.projection, null);
const acpMeta = await prepareSessionAcpMeta(readParams, deps.getConfig());
if (replacement || pending || !isCurrentGeneration(generation) || !neutral.projection.isCurrent() || !session.projection.isCurrent()) return;
return assemble(neutral.projection, session.projection, acpMeta);
};
const invalidate = (retainNotifiedFacts = false) => {
if (stoppedError) return;
if (!retainNotifiedFacts) lastNotifiedFacts = void 0;
invalidationEpoch += 1;
refreshVersion += 1;
pending?.generationReady.resolve();
pending = void 0;
current = void 0;
lastError = void 0;
if (!replacement) {
replacement = createDeferredCore();
replacement.promise.catch(() => {});
}
};
const fail = (error) => {
const replacementError = error instanceof Error ? error : new Error(formatErrorMessage(error));
refreshVersion += 1;
pending?.generationReady.resolve();
pending = void 0;
current = void 0;
lastError = replacementError;
const failedReplacement = replacement;
replacement = void 0;
failedReplacement?.reject(replacementError);
if (!stoppedError && lastSettlement !== invalidationEpoch) {
lastSettlement = invalidationEpoch;
notifyChanged();
}
};
const stop = async () => {
if (!stoppedError) {
stoppedError = new ChatMetadataSnapshotUnavailableError("gateway chat metadata runtime is stopped");
invalidationEpoch += 1;
fail(stoppedError);
requestedModelLists.clear();
}
while (activeWork.size > 0) await Promise.allSettled(activeWork);
};
return {
fail,
invalidate,
read: (readParams) => trackWork(read(readParams)),
readModelsList: (request) => trackWork(readModelsList(request)),
readStartup: (startupParams) => trackWork(readStartup(startupParams)),
refresh: (options) => trackWork(refresh(options)),
stop
};
}
//#endregion
export { createGatewayChatMetadataRuntime };