UNPKG

@gguf/claw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,346 lines (1,326 loc) 131 kB
import { t as __exportAll } from "./rolldown-runtime-Cbj13DAv.js"; import { g as resolveStateDir } from "./paths-B4BZAPZh.js"; import { E as truncateUtf16Safe, a as clampNumber, i as clampInt, y as resolveUserPath } from "./utils-CP9YLh6M.js"; import { t as createSubsystemLogger } from "./subsystem-BCQGGxdd.js"; import { c as resolveAgentWorkspaceDir, i as resolveAgentDir, r as resolveAgentConfig } from "./agent-scope-BnZW9Gh2.js"; import { G as requireApiKey, K as resolveApiKeyForProvider } from "./model-selection-CqaTAlhy.js"; import { t as isTruthyEnvValue } from "./env-VriqyjXT.js"; import { n as formatErrorMessage } from "./errors-kfGqPQ4b.js"; import { s as resolveSessionTranscriptsDirForAgent } from "./paths-C2NfoGZE.js"; import { n as onSessionTranscriptUpdate } from "./transcript-events-BwQWL-Af.js"; import { n as executeWithApiKeyRotation, r as parseGeminiAuth, t as collectProviderApiKeysForExecution } from "./api-key-rotation-CpGefdGL.js"; import { _ as statRegularFile, a as buildFileEntry, c as ensureDir, d as listMemoryFiles, f as normalizeExtraMemoryPaths, g as isFileMissingError, h as runWithConcurrency, i as sessionPathForFile, l as hashText, m as remapChunkLines, n as buildSessionEntry, o as chunkMarkdown, p as parseEmbedding, r as listSessionFilesForAgent, s as cosineSimilarity, t as requireNodeSqlite, u as isMemoryPath } from "./sqlite-JcMMx8Z5.js"; import { n as retryAsync } from "./retry-rUEdE6zT.js"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import fs$1 from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { createInterface } from "node:readline"; import { Readable } from "node:stream"; import chokidar from "chokidar"; //#region src/agents/memory-search.ts const DEFAULT_OPENAI_MODEL = "text-embedding-3-small"; const DEFAULT_GEMINI_MODEL = "gemini-embedding-001"; const DEFAULT_VOYAGE_MODEL = "voyage-4-large"; const DEFAULT_CHUNK_TOKENS = 400; const DEFAULT_CHUNK_OVERLAP = 80; const DEFAULT_WATCH_DEBOUNCE_MS = 1500; const DEFAULT_SESSION_DELTA_BYTES = 1e5; const DEFAULT_SESSION_DELTA_MESSAGES = 50; const DEFAULT_MAX_RESULTS = 6; const DEFAULT_MIN_SCORE = .35; const DEFAULT_HYBRID_ENABLED = true; const DEFAULT_HYBRID_VECTOR_WEIGHT = .7; const DEFAULT_HYBRID_TEXT_WEIGHT = .3; const DEFAULT_HYBRID_CANDIDATE_MULTIPLIER = 4; const DEFAULT_MMR_ENABLED = false; const DEFAULT_MMR_LAMBDA = .7; const DEFAULT_TEMPORAL_DECAY_ENABLED = false; const DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS = 30; const DEFAULT_CACHE_ENABLED = true; const DEFAULT_SOURCES = ["memory"]; function normalizeSources(sources, sessionMemoryEnabled) { const normalized = /* @__PURE__ */ new Set(); const input = sources?.length ? sources : DEFAULT_SOURCES; for (const source of input) { if (source === "memory") normalized.add("memory"); if (source === "sessions" && sessionMemoryEnabled) normalized.add("sessions"); } if (normalized.size === 0) normalized.add("memory"); return Array.from(normalized); } function resolveStorePath(agentId, raw) { const stateDir = resolveStateDir(process.env, os.homedir); const fallback = path.join(stateDir, "memory", `${agentId}.sqlite`); if (!raw) return fallback; return resolveUserPath(raw.includes("{agentId}") ? raw.replaceAll("{agentId}", agentId) : raw); } function mergeConfig(defaults, overrides, agentId) { const enabled = overrides?.enabled ?? defaults?.enabled ?? true; const sessionMemory = overrides?.experimental?.sessionMemory ?? defaults?.experimental?.sessionMemory ?? false; const provider = overrides?.provider ?? defaults?.provider ?? "auto"; const defaultRemote = defaults?.remote; const overrideRemote = overrides?.remote; const includeRemote = Boolean(overrideRemote?.baseUrl || overrideRemote?.apiKey || overrideRemote?.headers || defaultRemote?.baseUrl || defaultRemote?.apiKey || defaultRemote?.headers) || provider === "openai" || provider === "gemini" || provider === "voyage" || provider === "auto"; const batch = { enabled: overrideRemote?.batch?.enabled ?? defaultRemote?.batch?.enabled ?? false, wait: overrideRemote?.batch?.wait ?? defaultRemote?.batch?.wait ?? true, concurrency: Math.max(1, overrideRemote?.batch?.concurrency ?? defaultRemote?.batch?.concurrency ?? 2), pollIntervalMs: overrideRemote?.batch?.pollIntervalMs ?? defaultRemote?.batch?.pollIntervalMs ?? 2e3, timeoutMinutes: overrideRemote?.batch?.timeoutMinutes ?? defaultRemote?.batch?.timeoutMinutes ?? 60 }; const remote = includeRemote ? { baseUrl: overrideRemote?.baseUrl ?? defaultRemote?.baseUrl, apiKey: overrideRemote?.apiKey ?? defaultRemote?.apiKey, headers: overrideRemote?.headers ?? defaultRemote?.headers, batch } : void 0; const fallback = overrides?.fallback ?? defaults?.fallback ?? "none"; const modelDefault = provider === "gemini" ? DEFAULT_GEMINI_MODEL : provider === "openai" ? DEFAULT_OPENAI_MODEL : provider === "voyage" ? DEFAULT_VOYAGE_MODEL : void 0; const model = overrides?.model ?? defaults?.model ?? modelDefault ?? ""; const local = { modelPath: overrides?.local?.modelPath ?? defaults?.local?.modelPath, modelCacheDir: overrides?.local?.modelCacheDir ?? defaults?.local?.modelCacheDir }; const sources = normalizeSources(overrides?.sources ?? defaults?.sources, sessionMemory); const rawPaths = [...defaults?.extraPaths ?? [], ...overrides?.extraPaths ?? []].map((value) => value.trim()).filter(Boolean); const extraPaths = Array.from(new Set(rawPaths)); const vector = { enabled: overrides?.store?.vector?.enabled ?? defaults?.store?.vector?.enabled ?? true, extensionPath: overrides?.store?.vector?.extensionPath ?? defaults?.store?.vector?.extensionPath }; const store = { driver: overrides?.store?.driver ?? defaults?.store?.driver ?? "sqlite", path: resolveStorePath(agentId, overrides?.store?.path ?? defaults?.store?.path), vector }; const chunking = { tokens: overrides?.chunking?.tokens ?? defaults?.chunking?.tokens ?? DEFAULT_CHUNK_TOKENS, overlap: overrides?.chunking?.overlap ?? defaults?.chunking?.overlap ?? DEFAULT_CHUNK_OVERLAP }; const sync = { onSessionStart: overrides?.sync?.onSessionStart ?? defaults?.sync?.onSessionStart ?? true, onSearch: overrides?.sync?.onSearch ?? defaults?.sync?.onSearch ?? true, watch: overrides?.sync?.watch ?? defaults?.sync?.watch ?? true, watchDebounceMs: overrides?.sync?.watchDebounceMs ?? defaults?.sync?.watchDebounceMs ?? DEFAULT_WATCH_DEBOUNCE_MS, intervalMinutes: overrides?.sync?.intervalMinutes ?? defaults?.sync?.intervalMinutes ?? 0, sessions: { deltaBytes: overrides?.sync?.sessions?.deltaBytes ?? defaults?.sync?.sessions?.deltaBytes ?? DEFAULT_SESSION_DELTA_BYTES, deltaMessages: overrides?.sync?.sessions?.deltaMessages ?? defaults?.sync?.sessions?.deltaMessages ?? DEFAULT_SESSION_DELTA_MESSAGES } }; const query = { maxResults: overrides?.query?.maxResults ?? defaults?.query?.maxResults ?? DEFAULT_MAX_RESULTS, minScore: overrides?.query?.minScore ?? defaults?.query?.minScore ?? DEFAULT_MIN_SCORE }; const hybrid = { enabled: overrides?.query?.hybrid?.enabled ?? defaults?.query?.hybrid?.enabled ?? DEFAULT_HYBRID_ENABLED, vectorWeight: overrides?.query?.hybrid?.vectorWeight ?? defaults?.query?.hybrid?.vectorWeight ?? DEFAULT_HYBRID_VECTOR_WEIGHT, textWeight: overrides?.query?.hybrid?.textWeight ?? defaults?.query?.hybrid?.textWeight ?? DEFAULT_HYBRID_TEXT_WEIGHT, candidateMultiplier: overrides?.query?.hybrid?.candidateMultiplier ?? defaults?.query?.hybrid?.candidateMultiplier ?? DEFAULT_HYBRID_CANDIDATE_MULTIPLIER, mmr: { enabled: overrides?.query?.hybrid?.mmr?.enabled ?? defaults?.query?.hybrid?.mmr?.enabled ?? DEFAULT_MMR_ENABLED, lambda: overrides?.query?.hybrid?.mmr?.lambda ?? defaults?.query?.hybrid?.mmr?.lambda ?? DEFAULT_MMR_LAMBDA }, temporalDecay: { enabled: overrides?.query?.hybrid?.temporalDecay?.enabled ?? defaults?.query?.hybrid?.temporalDecay?.enabled ?? DEFAULT_TEMPORAL_DECAY_ENABLED, halfLifeDays: overrides?.query?.hybrid?.temporalDecay?.halfLifeDays ?? defaults?.query?.hybrid?.temporalDecay?.halfLifeDays ?? DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS } }; const cache = { enabled: overrides?.cache?.enabled ?? defaults?.cache?.enabled ?? DEFAULT_CACHE_ENABLED, maxEntries: overrides?.cache?.maxEntries ?? defaults?.cache?.maxEntries }; const overlap = clampNumber(chunking.overlap, 0, Math.max(0, chunking.tokens - 1)); const minScore = clampNumber(query.minScore, 0, 1); const vectorWeight = clampNumber(hybrid.vectorWeight, 0, 1); const textWeight = clampNumber(hybrid.textWeight, 0, 1); const sum = vectorWeight + textWeight; const normalizedVectorWeight = sum > 0 ? vectorWeight / sum : DEFAULT_HYBRID_VECTOR_WEIGHT; const normalizedTextWeight = sum > 0 ? textWeight / sum : DEFAULT_HYBRID_TEXT_WEIGHT; const candidateMultiplier = clampInt(hybrid.candidateMultiplier, 1, 20); const temporalDecayHalfLifeDays = Math.max(1, Math.floor(Number.isFinite(hybrid.temporalDecay.halfLifeDays) ? hybrid.temporalDecay.halfLifeDays : DEFAULT_TEMPORAL_DECAY_HALF_LIFE_DAYS)); const deltaBytes = clampInt(sync.sessions.deltaBytes, 0, Number.MAX_SAFE_INTEGER); const deltaMessages = clampInt(sync.sessions.deltaMessages, 0, Number.MAX_SAFE_INTEGER); return { enabled, sources, extraPaths, provider, remote, experimental: { sessionMemory }, fallback, model, local, store, chunking: { tokens: Math.max(1, chunking.tokens), overlap }, sync: { ...sync, sessions: { deltaBytes, deltaMessages } }, query: { ...query, minScore, hybrid: { enabled: Boolean(hybrid.enabled), vectorWeight: normalizedVectorWeight, textWeight: normalizedTextWeight, candidateMultiplier, mmr: { enabled: Boolean(hybrid.mmr.enabled), lambda: Number.isFinite(hybrid.mmr.lambda) ? Math.max(0, Math.min(1, hybrid.mmr.lambda)) : DEFAULT_MMR_LAMBDA }, temporalDecay: { enabled: Boolean(hybrid.temporalDecay.enabled), halfLifeDays: temporalDecayHalfLifeDays } } }, cache: { enabled: Boolean(cache.enabled), maxEntries: typeof cache.maxEntries === "number" && Number.isFinite(cache.maxEntries) ? Math.max(1, Math.floor(cache.maxEntries)) : void 0 } }; } function resolveMemorySearchConfig(cfg, agentId) { const defaults = cfg.agents?.defaults?.memorySearch; const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch; const resolved = mergeConfig(defaults, overrides, agentId); if (!resolved.enabled) return null; return resolved; } //#endregion //#region src/memory/embeddings-debug.ts const debugEmbeddings = isTruthyEnvValue(process.env.OPENCLAW_DEBUG_MEMORY_EMBEDDINGS); const log$3 = createSubsystemLogger("memory/embeddings"); function debugEmbeddingsLog(message, meta) { if (!debugEmbeddings) return; const suffix = meta ? ` ${JSON.stringify(meta)}` : ""; log$3.raw(`${message}${suffix}`); } //#endregion //#region src/memory/embeddings-gemini.ts const DEFAULT_GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; const DEFAULT_GEMINI_EMBEDDING_MODEL = "gemini-embedding-001"; const GEMINI_MAX_INPUT_TOKENS = { "text-embedding-004": 2048 }; function resolveRemoteApiKey(remoteApiKey) { const trimmed = remoteApiKey?.trim(); if (!trimmed) return; if (trimmed === "GOOGLE_API_KEY" || trimmed === "GEMINI_API_KEY") return process.env[trimmed]?.trim(); return trimmed; } function normalizeGeminiModel(model) { const trimmed = model.trim(); if (!trimmed) return DEFAULT_GEMINI_EMBEDDING_MODEL; const withoutPrefix = trimmed.replace(/^models\//, ""); if (withoutPrefix.startsWith("gemini/")) return withoutPrefix.slice(7); if (withoutPrefix.startsWith("google/")) return withoutPrefix.slice(7); return withoutPrefix; } function normalizeGeminiBaseUrl(raw) { const trimmed = raw.replace(/\/+$/, ""); const openAiIndex = trimmed.indexOf("/openai"); if (openAiIndex > -1) return trimmed.slice(0, openAiIndex); return trimmed; } function buildGeminiModelPath(model) { return model.startsWith("models/") ? model : `models/${model}`; } async function createGeminiEmbeddingProvider(options) { const client = await resolveGeminiEmbeddingClient(options); const baseUrl = client.baseUrl.replace(/\/$/, ""); const embedUrl = `${baseUrl}/${client.modelPath}:embedContent`; const batchUrl = `${baseUrl}/${client.modelPath}:batchEmbedContents`; const fetchWithGeminiAuth = async (apiKey, endpoint, body) => { const headers = { ...parseGeminiAuth(apiKey).headers, ...client.headers }; const res = await fetch(endpoint, { method: "POST", headers, body: JSON.stringify(body) }); if (!res.ok) { const payload = await res.text(); throw new Error(`gemini embeddings failed: ${res.status} ${payload}`); } return await res.json(); }; const embedQuery = async (text) => { if (!text.trim()) return []; return (await executeWithApiKeyRotation({ provider: "google", apiKeys: client.apiKeys, execute: (apiKey) => fetchWithGeminiAuth(apiKey, embedUrl, { content: { parts: [{ text }] }, taskType: "RETRIEVAL_QUERY" }) })).embedding?.values ?? []; }; const embedBatch = async (texts) => { if (texts.length === 0) return []; const requests = texts.map((text) => ({ model: client.modelPath, content: { parts: [{ text }] }, taskType: "RETRIEVAL_DOCUMENT" })); const payload = await executeWithApiKeyRotation({ provider: "google", apiKeys: client.apiKeys, execute: (apiKey) => fetchWithGeminiAuth(apiKey, batchUrl, { requests }) }); const embeddings = Array.isArray(payload.embeddings) ? payload.embeddings : []; return texts.map((_, index) => embeddings[index]?.values ?? []); }; return { provider: { id: "gemini", model: client.model, maxInputTokens: GEMINI_MAX_INPUT_TOKENS[client.model], embedQuery, embedBatch }, client }; } async function resolveGeminiEmbeddingClient(options) { const remote = options.remote; const remoteApiKey = resolveRemoteApiKey(remote?.apiKey); const remoteBaseUrl = remote?.baseUrl?.trim(); const apiKey = remoteApiKey ? remoteApiKey : requireApiKey(await resolveApiKeyForProvider({ provider: "google", cfg: options.config, agentDir: options.agentDir }), "google"); const providerConfig = options.config.models?.providers?.google; const rawBaseUrl = remoteBaseUrl || providerConfig?.baseUrl?.trim() || DEFAULT_GEMINI_BASE_URL; const baseUrl = normalizeGeminiBaseUrl(rawBaseUrl); const headers = { ...Object.assign({}, providerConfig?.headers, remote?.headers) }; const apiKeys = collectProviderApiKeysForExecution({ provider: "google", primaryApiKey: apiKey }); const model = normalizeGeminiModel(options.model); const modelPath = buildGeminiModelPath(model); debugEmbeddingsLog("memory embeddings: gemini client", { rawBaseUrl, baseUrl, model, modelPath, embedEndpoint: `${baseUrl}/${modelPath}:embedContent`, batchEndpoint: `${baseUrl}/${modelPath}:batchEmbedContents` }); return { baseUrl, headers, model, modelPath, apiKeys }; } //#endregion //#region src/memory/embeddings-remote-client.ts async function resolveRemoteEmbeddingBearerClient(params) { const remote = params.options.remote; const remoteApiKey = remote?.apiKey?.trim(); const remoteBaseUrl = remote?.baseUrl?.trim(); const providerConfig = params.options.config.models?.providers?.[params.provider]; const apiKey = remoteApiKey ? remoteApiKey : requireApiKey(await resolveApiKeyForProvider({ provider: params.provider, cfg: params.options.config, agentDir: params.options.agentDir }), params.provider); const baseUrl = remoteBaseUrl || providerConfig?.baseUrl?.trim() || params.defaultBaseUrl; const headerOverrides = Object.assign({}, providerConfig?.headers, remote?.headers); return { baseUrl, headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, ...headerOverrides } }; } //#endregion //#region src/memory/embeddings-remote-fetch.ts async function fetchRemoteEmbeddingVectors(params) { const res = await fetch(params.url, { method: "POST", headers: params.headers, body: JSON.stringify(params.body) }); if (!res.ok) { const text = await res.text(); throw new Error(`${params.errorPrefix}: ${res.status} ${text}`); } return ((await res.json()).data ?? []).map((entry) => entry.embedding ?? []); } //#endregion //#region src/memory/embeddings-openai.ts const DEFAULT_OPENAI_EMBEDDING_MODEL = "text-embedding-3-small"; const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1"; const OPENAI_MAX_INPUT_TOKENS = { "text-embedding-3-small": 8192, "text-embedding-3-large": 8192, "text-embedding-ada-002": 8191 }; function normalizeOpenAiModel(model) { const trimmed = model.trim(); if (!trimmed) return DEFAULT_OPENAI_EMBEDDING_MODEL; if (trimmed.startsWith("openai/")) return trimmed.slice(7); return trimmed; } async function createOpenAiEmbeddingProvider(options) { const client = await resolveOpenAiEmbeddingClient(options); const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`; const embed = async (input) => { if (input.length === 0) return []; return await fetchRemoteEmbeddingVectors({ url, headers: client.headers, body: { model: client.model, input }, errorPrefix: "openai embeddings failed" }); }; return { provider: { id: "openai", model: client.model, maxInputTokens: OPENAI_MAX_INPUT_TOKENS[client.model], embedQuery: async (text) => { const [vec] = await embed([text]); return vec ?? []; }, embedBatch: embed }, client }; } async function resolveOpenAiEmbeddingClient(options) { const { baseUrl, headers } = await resolveRemoteEmbeddingBearerClient({ provider: "openai", options, defaultBaseUrl: DEFAULT_OPENAI_BASE_URL }); return { baseUrl, headers, model: normalizeOpenAiModel(options.model) }; } //#endregion //#region src/memory/embeddings-voyage.ts const DEFAULT_VOYAGE_EMBEDDING_MODEL = "voyage-4-large"; const DEFAULT_VOYAGE_BASE_URL = "https://api.voyageai.com/v1"; const VOYAGE_MAX_INPUT_TOKENS = { "voyage-3": 32e3, "voyage-3-lite": 16e3, "voyage-code-3": 32e3 }; function normalizeVoyageModel(model) { const trimmed = model.trim(); if (!trimmed) return DEFAULT_VOYAGE_EMBEDDING_MODEL; if (trimmed.startsWith("voyage/")) return trimmed.slice(7); return trimmed; } async function createVoyageEmbeddingProvider(options) { const client = await resolveVoyageEmbeddingClient(options); const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`; const embed = async (input, input_type) => { if (input.length === 0) return []; const body = { model: client.model, input }; if (input_type) body.input_type = input_type; return await fetchRemoteEmbeddingVectors({ url, headers: client.headers, body, errorPrefix: "voyage embeddings failed" }); }; return { provider: { id: "voyage", model: client.model, maxInputTokens: VOYAGE_MAX_INPUT_TOKENS[client.model], embedQuery: async (text) => { const [vec] = await embed([text], "query"); return vec ?? []; }, embedBatch: async (texts) => embed(texts, "document") }, client }; } async function resolveVoyageEmbeddingClient(options) { const { baseUrl, headers } = await resolveRemoteEmbeddingBearerClient({ provider: "voyage", options, defaultBaseUrl: DEFAULT_VOYAGE_BASE_URL }); return { baseUrl, headers, model: normalizeVoyageModel(options.model) }; } //#endregion //#region src/memory/node-llama.ts async function importNodeLlamaCpp() { return import("node-llama-cpp"); } //#endregion //#region src/memory/embeddings.ts function sanitizeAndNormalizeEmbedding(vec) { const sanitized = vec.map((value) => Number.isFinite(value) ? value : 0); const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0)); if (magnitude < 1e-10) return sanitized; return sanitized.map((value) => value / magnitude); } const REMOTE_EMBEDDING_PROVIDER_IDS = [ "openai", "gemini", "voyage" ]; const DEFAULT_LOCAL_MODEL = "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf"; function canAutoSelectLocal(options) { const modelPath = options.local?.modelPath?.trim(); if (!modelPath) return false; if (/^(hf:|https?:)/i.test(modelPath)) return false; const resolved = resolveUserPath(modelPath); try { return fs.statSync(resolved).isFile(); } catch { return false; } } function isMissingApiKeyError(err) { return formatErrorMessage(err).includes("No API key found for provider"); } async function createLocalEmbeddingProvider(options) { const modelPath = options.local?.modelPath?.trim() || DEFAULT_LOCAL_MODEL; const modelCacheDir = options.local?.modelCacheDir?.trim(); const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp(); let llama = null; let embeddingModel = null; let embeddingContext = null; const ensureContext = async () => { if (!llama) llama = await getLlama({ logLevel: LlamaLogLevel.error }); if (!embeddingModel) { const resolved = await resolveModelFile(modelPath, modelCacheDir || void 0); embeddingModel = await llama.loadModel({ modelPath: resolved }); } if (!embeddingContext) embeddingContext = await embeddingModel.createEmbeddingContext(); return embeddingContext; }; return { id: "local", model: modelPath, embedQuery: async (text) => { const embedding = await (await ensureContext()).getEmbeddingFor(text); return sanitizeAndNormalizeEmbedding(Array.from(embedding.vector)); }, embedBatch: async (texts) => { const ctx = await ensureContext(); return await Promise.all(texts.map(async (text) => { const embedding = await ctx.getEmbeddingFor(text); return sanitizeAndNormalizeEmbedding(Array.from(embedding.vector)); })); } }; } async function createEmbeddingProvider(options) { const requestedProvider = options.provider; const fallback = options.fallback; const createProvider = async (id) => { if (id === "local") return { provider: await createLocalEmbeddingProvider(options) }; if (id === "gemini") { const { provider, client } = await createGeminiEmbeddingProvider(options); return { provider, gemini: client }; } if (id === "voyage") { const { provider, client } = await createVoyageEmbeddingProvider(options); return { provider, voyage: client }; } const { provider, client } = await createOpenAiEmbeddingProvider(options); return { provider, openAi: client }; }; const formatPrimaryError = (err, provider) => provider === "local" ? formatLocalSetupError(err) : formatErrorMessage(err); if (requestedProvider === "auto") { const missingKeyErrors = []; let localError = null; if (canAutoSelectLocal(options)) try { return { ...await createProvider("local"), requestedProvider }; } catch (err) { localError = formatLocalSetupError(err); } for (const provider of REMOTE_EMBEDDING_PROVIDER_IDS) try { return { ...await createProvider(provider), requestedProvider }; } catch (err) { const message = formatPrimaryError(err, provider); if (isMissingApiKeyError(err)) { missingKeyErrors.push(message); continue; } throw new Error(message, { cause: err }); } const details = [...missingKeyErrors, localError].filter(Boolean); return { provider: null, requestedProvider, providerUnavailableReason: details.length > 0 ? details.join("\n\n") : "No embeddings provider available." }; } try { return { ...await createProvider(requestedProvider), requestedProvider }; } catch (primaryErr) { const reason = formatPrimaryError(primaryErr, requestedProvider); if (fallback && fallback !== "none" && fallback !== requestedProvider) try { return { ...await createProvider(fallback), requestedProvider, fallbackFrom: requestedProvider, fallbackReason: reason }; } catch (fallbackErr) { const combinedReason = `${reason}\n\nFallback to ${fallback} failed: ${formatErrorMessage(fallbackErr)}`; if (isMissingApiKeyError(primaryErr) && isMissingApiKeyError(fallbackErr)) return { provider: null, requestedProvider, fallbackFrom: requestedProvider, fallbackReason: reason, providerUnavailableReason: combinedReason }; throw new Error(combinedReason, { cause: fallbackErr }); } if (isMissingApiKeyError(primaryErr)) return { provider: null, requestedProvider, providerUnavailableReason: reason }; throw new Error(reason, { cause: primaryErr }); } } function isNodeLlamaCppMissing(err) { if (!(err instanceof Error)) return false; if (err.code === "ERR_MODULE_NOT_FOUND") return err.message.includes("node-llama-cpp"); return false; } function formatLocalSetupError(err) { const detail = formatErrorMessage(err); const missing = isNodeLlamaCppMissing(err); return [ "Local embeddings unavailable.", missing ? "Reason: optional dependency node-llama-cpp is missing (or failed to install)." : detail ? `Reason: ${detail}` : void 0, missing && detail ? `Detail: ${detail}` : null, "To enable local embeddings:", "1) Use Node 22 LTS (recommended for installs/updates)", missing ? "2) Reinstall OpenClaw (this should install node-llama-cpp): npm i -g openclaw@latest" : null, "3) If you use pnpm: pnpm approve-builds (select node-llama-cpp), then pnpm rebuild node-llama-cpp", ...REMOTE_EMBEDDING_PROVIDER_IDS.map((provider) => `Or set agents.defaults.memorySearch.provider = "${provider}" (remote).`) ].filter(Boolean).join("\n"); } //#endregion //#region src/memory/mmr.ts const DEFAULT_MMR_CONFIG = { enabled: false, lambda: .7 }; /** * Tokenize text for Jaccard similarity computation. * Extracts alphanumeric tokens and normalizes to lowercase. */ function tokenize$1(text) { const tokens = text.toLowerCase().match(/[a-z0-9_]+/g) ?? []; return new Set(tokens); } /** * Compute Jaccard similarity between two token sets. * Returns a value in [0, 1] where 1 means identical sets. */ function jaccardSimilarity(setA, setB) { if (setA.size === 0 && setB.size === 0) return 1; if (setA.size === 0 || setB.size === 0) return 0; let intersectionSize = 0; const smaller = setA.size <= setB.size ? setA : setB; const larger = setA.size <= setB.size ? setB : setA; for (const token of smaller) if (larger.has(token)) intersectionSize++; const unionSize = setA.size + setB.size - intersectionSize; return unionSize === 0 ? 0 : intersectionSize / unionSize; } /** * Compute the maximum similarity between an item and all selected items. */ function maxSimilarityToSelected(item, selectedItems, tokenCache) { if (selectedItems.length === 0) return 0; let maxSim = 0; const itemTokens = tokenCache.get(item.id) ?? tokenize$1(item.content); for (const selected of selectedItems) { const sim = jaccardSimilarity(itemTokens, tokenCache.get(selected.id) ?? tokenize$1(selected.content)); if (sim > maxSim) maxSim = sim; } return maxSim; } /** * Compute MMR score for a candidate item. * MMR = λ * relevance - (1-λ) * max_similarity_to_selected */ function computeMMRScore(relevance, maxSimilarity, lambda) { return lambda * relevance - (1 - lambda) * maxSimilarity; } /** * Re-rank items using Maximal Marginal Relevance (MMR). * * The algorithm iteratively selects items that balance relevance with diversity: * 1. Start with the highest-scoring item * 2. For each remaining slot, select the item that maximizes the MMR score * 3. MMR score = λ * relevance - (1-λ) * max_similarity_to_already_selected * * @param items - Items to re-rank, must have score and content * @param config - MMR configuration (lambda, enabled) * @returns Re-ranked items in MMR order */ function mmrRerank(items, config = {}) { const { enabled = DEFAULT_MMR_CONFIG.enabled, lambda = DEFAULT_MMR_CONFIG.lambda } = config; if (!enabled || items.length <= 1) return [...items]; const clampedLambda = Math.max(0, Math.min(1, lambda)); if (clampedLambda === 1) return [...items].toSorted((a, b) => b.score - a.score); const tokenCache = /* @__PURE__ */ new Map(); for (const item of items) tokenCache.set(item.id, tokenize$1(item.content)); const maxScore = Math.max(...items.map((i) => i.score)); const minScore = Math.min(...items.map((i) => i.score)); const scoreRange = maxScore - minScore; const normalizeScore = (score) => { if (scoreRange === 0) return 1; return (score - minScore) / scoreRange; }; const selected = []; const remaining = new Set(items); while (remaining.size > 0) { let bestItem = null; let bestMMRScore = -Infinity; for (const candidate of remaining) { const mmrScore = computeMMRScore(normalizeScore(candidate.score), maxSimilarityToSelected(candidate, selected, tokenCache), clampedLambda); if (mmrScore > bestMMRScore || mmrScore === bestMMRScore && candidate.score > (bestItem?.score ?? -Infinity)) { bestMMRScore = mmrScore; bestItem = candidate; } } if (bestItem) { selected.push(bestItem); remaining.delete(bestItem); } else break; } return selected; } /** * Apply MMR re-ranking to hybrid search results. * Adapts the generic MMR function to work with the hybrid search result format. */ function applyMMRToHybridResults(results, config = {}) { if (results.length === 0) return results; const itemById = /* @__PURE__ */ new Map(); return mmrRerank(results.map((r, index) => { const id = `${r.path}:${r.startLine}:${index}`; itemById.set(id, r); return { id, score: r.score, content: r.snippet }; }), config).map((item) => itemById.get(item.id)); } //#endregion //#region src/memory/temporal-decay.ts const DEFAULT_TEMPORAL_DECAY_CONFIG = { enabled: false, halfLifeDays: 30 }; const DAY_MS = 1440 * 60 * 1e3; const DATED_MEMORY_PATH_RE = /(?:^|\/)memory\/(\d{4})-(\d{2})-(\d{2})\.md$/; function toDecayLambda(halfLifeDays) { if (!Number.isFinite(halfLifeDays) || halfLifeDays <= 0) return 0; return Math.LN2 / halfLifeDays; } function calculateTemporalDecayMultiplier(params) { const lambda = toDecayLambda(params.halfLifeDays); const clampedAge = Math.max(0, params.ageInDays); if (lambda <= 0 || !Number.isFinite(clampedAge)) return 1; return Math.exp(-lambda * clampedAge); } function applyTemporalDecayToScore(params) { return params.score * calculateTemporalDecayMultiplier(params); } function parseMemoryDateFromPath(filePath) { const normalized = filePath.replaceAll("\\", "/").replace(/^\.\//, ""); const match = DATED_MEMORY_PATH_RE.exec(normalized); if (!match) return null; const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day)) return null; const timestamp = Date.UTC(year, month - 1, day); const parsed = new Date(timestamp); if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() !== month - 1 || parsed.getUTCDate() !== day) return null; return parsed; } function isEvergreenMemoryPath(filePath) { const normalized = filePath.replaceAll("\\", "/").replace(/^\.\//, ""); if (normalized === "MEMORY.md" || normalized === "memory.md") return true; if (!normalized.startsWith("memory/")) return false; return !DATED_MEMORY_PATH_RE.test(normalized); } async function extractTimestamp(params) { const fromPath = parseMemoryDateFromPath(params.filePath); if (fromPath) return fromPath; if (params.source === "memory" && isEvergreenMemoryPath(params.filePath)) return null; if (!params.workspaceDir) return null; const absolutePath = path.isAbsolute(params.filePath) ? params.filePath : path.resolve(params.workspaceDir, params.filePath); try { const stat = await fs$1.stat(absolutePath); if (!Number.isFinite(stat.mtimeMs)) return null; return new Date(stat.mtimeMs); } catch { return null; } } function ageInDaysFromTimestamp(timestamp, nowMs) { return Math.max(0, nowMs - timestamp.getTime()) / DAY_MS; } async function applyTemporalDecayToHybridResults(params) { const config = { ...DEFAULT_TEMPORAL_DECAY_CONFIG, ...params.temporalDecay }; if (!config.enabled) return [...params.results]; const nowMs = params.nowMs ?? Date.now(); const timestampPromiseCache = /* @__PURE__ */ new Map(); return Promise.all(params.results.map(async (entry) => { const cacheKey = `${entry.source}:${entry.path}`; let timestampPromise = timestampPromiseCache.get(cacheKey); if (!timestampPromise) { timestampPromise = extractTimestamp({ filePath: entry.path, source: entry.source, workspaceDir: params.workspaceDir }); timestampPromiseCache.set(cacheKey, timestampPromise); } const timestamp = await timestampPromise; if (!timestamp) return entry; const decayedScore = applyTemporalDecayToScore({ score: entry.score, ageInDays: ageInDaysFromTimestamp(timestamp, nowMs), halfLifeDays: config.halfLifeDays }); return { ...entry, score: decayedScore }; })); } //#endregion //#region src/memory/hybrid.ts function buildFtsQuery(raw) { const tokens = raw.match(/[\p{L}\p{N}_]+/gu)?.map((t) => t.trim()).filter(Boolean) ?? []; if (tokens.length === 0) return null; return tokens.map((t) => `"${t.replaceAll("\"", "")}"`).join(" AND "); } function bm25RankToScore(rank) { return 1 / (1 + (Number.isFinite(rank) ? Math.max(0, rank) : 999)); } async function mergeHybridResults(params) { const byId = /* @__PURE__ */ new Map(); for (const r of params.vector) byId.set(r.id, { id: r.id, path: r.path, startLine: r.startLine, endLine: r.endLine, source: r.source, snippet: r.snippet, vectorScore: r.vectorScore, textScore: 0 }); for (const r of params.keyword) { const existing = byId.get(r.id); if (existing) { existing.textScore = r.textScore; if (r.snippet && r.snippet.length > 0) existing.snippet = r.snippet; } else byId.set(r.id, { id: r.id, path: r.path, startLine: r.startLine, endLine: r.endLine, source: r.source, snippet: r.snippet, vectorScore: 0, textScore: r.textScore }); } const sorted = (await applyTemporalDecayToHybridResults({ results: Array.from(byId.values()).map((entry) => { const score = params.vectorWeight * entry.vectorScore + params.textWeight * entry.textScore; return { path: entry.path, startLine: entry.startLine, endLine: entry.endLine, score, snippet: entry.snippet, source: entry.source }; }), temporalDecay: { ...DEFAULT_TEMPORAL_DECAY_CONFIG, ...params.temporalDecay }, workspaceDir: params.workspaceDir, nowMs: params.nowMs })).toSorted((a, b) => b.score - a.score); const mmrConfig = { ...DEFAULT_MMR_CONFIG, ...params.mmr }; if (mmrConfig.enabled) return applyMMRToHybridResults(sorted, mmrConfig); return sorted; } //#endregion //#region src/memory/batch-utils.ts function normalizeBatchBaseUrl(client) { return client.baseUrl?.replace(/\/$/, "") ?? ""; } function buildBatchHeaders(client, params) { const headers = client.headers ? { ...client.headers } : {}; if (params.json) { if (!headers["Content-Type"] && !headers["content-type"]) headers["Content-Type"] = "application/json"; } else { delete headers["Content-Type"]; delete headers["content-type"]; } return headers; } function splitBatchRequests(requests, maxRequests) { if (requests.length <= maxRequests) return [requests]; const groups = []; for (let i = 0; i < requests.length; i += maxRequests) groups.push(requests.slice(i, i + maxRequests)); return groups; } //#endregion //#region src/memory/batch-runner.ts async function runEmbeddingBatchGroups(params) { if (params.requests.length === 0) return /* @__PURE__ */ new Map(); const groups = splitBatchRequests(params.requests, params.maxRequests); const byCustomId = /* @__PURE__ */ new Map(); const tasks = groups.map((group, groupIndex) => async () => { await params.runGroup({ group, groupIndex, groups: groups.length, byCustomId }); }); params.debug?.(params.debugLabel, { requests: params.requests.length, groups: groups.length, wait: params.wait, concurrency: params.concurrency, pollIntervalMs: params.pollIntervalMs, timeoutMs: params.timeoutMs }); await runWithConcurrency(tasks, params.concurrency); return byCustomId; } //#endregion //#region src/memory/batch-gemini.ts const GEMINI_BATCH_MAX_REQUESTS = 5e4; function getGeminiUploadUrl(baseUrl) { if (baseUrl.includes("/v1beta")) return baseUrl.replace(/\/v1beta\/?$/, "/upload/v1beta"); return `${baseUrl.replace(/\/$/, "")}/upload`; } function buildGeminiUploadBody(params) { const boundary = `openclaw-${hashText(params.displayName)}`; const jsonPart = JSON.stringify({ file: { displayName: params.displayName, mimeType: "application/jsonl" } }); const delimiter = `--${boundary}\r\n`; const closeDelimiter = `--${boundary}--\r\n`; const parts = [ `${delimiter}Content-Type: application/json; charset=UTF-8\r\n\r\n${jsonPart}\r\n`, `${delimiter}Content-Type: application/jsonl; charset=UTF-8\r\n\r\n${params.jsonl}\r\n`, closeDelimiter ]; return { body: new Blob([parts.join("")], { type: "multipart/related" }), contentType: `multipart/related; boundary=${boundary}` }; } async function submitGeminiBatch(params) { const baseUrl = normalizeBatchBaseUrl(params.gemini); const uploadPayload = buildGeminiUploadBody({ jsonl: params.requests.map((request) => JSON.stringify({ key: request.custom_id, request: { content: request.content, task_type: request.taskType } })).join("\n"), displayName: `memory-embeddings-${hashText(String(Date.now()))}` }); const uploadUrl = `${getGeminiUploadUrl(baseUrl)}/files?uploadType=multipart`; debugEmbeddingsLog("memory embeddings: gemini batch upload", { uploadUrl, baseUrl, requests: params.requests.length }); const fileRes = await fetch(uploadUrl, { method: "POST", headers: { ...buildBatchHeaders(params.gemini, { json: false }), "Content-Type": uploadPayload.contentType }, body: uploadPayload.body }); if (!fileRes.ok) { const text = await fileRes.text(); throw new Error(`gemini batch file upload failed: ${fileRes.status} ${text}`); } const filePayload = await fileRes.json(); const fileId = filePayload.name ?? filePayload.file?.name; if (!fileId) throw new Error("gemini batch file upload failed: missing file id"); const batchBody = { batch: { displayName: `memory-embeddings-${params.agentId}`, inputConfig: { file_name: fileId } } }; const batchEndpoint = `${baseUrl}/${params.gemini.modelPath}:asyncBatchEmbedContent`; debugEmbeddingsLog("memory embeddings: gemini batch create", { batchEndpoint, fileId }); const batchRes = await fetch(batchEndpoint, { method: "POST", headers: buildBatchHeaders(params.gemini, { json: true }), body: JSON.stringify(batchBody) }); if (batchRes.ok) return await batchRes.json(); const text = await batchRes.text(); if (batchRes.status === 404) throw new Error("gemini batch create failed: 404 (asyncBatchEmbedContent not available for this model/baseUrl). Disable remote.batch.enabled or switch providers."); throw new Error(`gemini batch create failed: ${batchRes.status} ${text}`); } async function fetchGeminiBatchStatus(params) { const statusUrl = `${normalizeBatchBaseUrl(params.gemini)}/${params.batchName.startsWith("batches/") ? params.batchName : `batches/${params.batchName}`}`; debugEmbeddingsLog("memory embeddings: gemini batch status", { statusUrl }); const res = await fetch(statusUrl, { headers: buildBatchHeaders(params.gemini, { json: true }) }); if (!res.ok) { const text = await res.text(); throw new Error(`gemini batch status failed: ${res.status} ${text}`); } return await res.json(); } async function fetchGeminiFileContent(params) { const downloadUrl = `${normalizeBatchBaseUrl(params.gemini)}/${params.fileId.startsWith("files/") ? params.fileId : `files/${params.fileId}`}:download`; debugEmbeddingsLog("memory embeddings: gemini batch download", { downloadUrl }); const res = await fetch(downloadUrl, { headers: buildBatchHeaders(params.gemini, { json: true }) }); if (!res.ok) { const text = await res.text(); throw new Error(`gemini batch file content failed: ${res.status} ${text}`); } return await res.text(); } function parseGeminiBatchOutput(text) { if (!text.trim()) return []; return text.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line)); } async function waitForGeminiBatch(params) { const start = Date.now(); let current = params.initial; while (true) { const status = current ?? await fetchGeminiBatchStatus({ gemini: params.gemini, batchName: params.batchName }); const state = status.state ?? "UNKNOWN"; if ([ "SUCCEEDED", "COMPLETED", "DONE" ].includes(state)) { const outputFileId = status.outputConfig?.file ?? status.outputConfig?.fileId ?? status.metadata?.output?.responsesFile; if (!outputFileId) throw new Error(`gemini batch ${params.batchName} completed without output file`); return { outputFileId }; } if ([ "FAILED", "CANCELLED", "CANCELED", "EXPIRED" ].includes(state)) { const message = status.error?.message ?? "unknown error"; throw new Error(`gemini batch ${params.batchName} ${state}: ${message}`); } if (!params.wait) throw new Error(`gemini batch ${params.batchName} still ${state}; wait disabled`); if (Date.now() - start > params.timeoutMs) throw new Error(`gemini batch ${params.batchName} timed out after ${params.timeoutMs}ms`); params.debug?.(`gemini batch ${params.batchName} ${state}; waiting ${params.pollIntervalMs}ms`); await new Promise((resolve) => setTimeout(resolve, params.pollIntervalMs)); current = void 0; } } async function runGeminiEmbeddingBatches(params) { return await runEmbeddingBatchGroups({ requests: params.requests, maxRequests: GEMINI_BATCH_MAX_REQUESTS, wait: params.wait, pollIntervalMs: params.pollIntervalMs, timeoutMs: params.timeoutMs, concurrency: params.concurrency, debug: params.debug, debugLabel: "memory embeddings: gemini batch submit", runGroup: async ({ group, groupIndex, groups, byCustomId }) => { const batchInfo = await submitGeminiBatch({ gemini: params.gemini, requests: group, agentId: params.agentId }); const batchName = batchInfo.name ?? ""; if (!batchName) throw new Error("gemini batch create failed: missing batch name"); params.debug?.("memory embeddings: gemini batch created", { batchName, state: batchInfo.state, group: groupIndex + 1, groups, requests: group.length }); if (!params.wait && batchInfo.state && ![ "SUCCEEDED", "COMPLETED", "DONE" ].includes(batchInfo.state)) throw new Error(`gemini batch ${batchName} submitted; enable remote.batch.wait to await completion`); const completed = batchInfo.state && [ "SUCCEEDED", "COMPLETED", "DONE" ].includes(batchInfo.state) ? { outputFileId: batchInfo.outputConfig?.file ?? batchInfo.outputConfig?.fileId ?? batchInfo.metadata?.output?.responsesFile ?? "" } : await waitForGeminiBatch({ gemini: params.gemini, batchName, wait: params.wait, pollIntervalMs: params.pollIntervalMs, timeoutMs: params.timeoutMs, debug: params.debug, initial: batchInfo }); if (!completed.outputFileId) throw new Error(`gemini batch ${batchName} completed without output file`); const outputLines = parseGeminiBatchOutput(await fetchGeminiFileContent({ gemini: params.gemini, fileId: completed.outputFileId })); const errors = []; const remaining = new Set(group.map((request) => request.custom_id)); for (const line of outputLines) { const customId = line.key ?? line.custom_id ?? line.request_id; if (!customId) continue; remaining.delete(customId); if (line.error?.message) { errors.push(`${customId}: ${line.error.message}`); continue; } if (line.response?.error?.message) { errors.push(`${customId}: ${line.response.error.message}`); continue; } const embedding = line.embedding?.values ?? line.response?.embedding?.values ?? []; if (embedding.length === 0) { errors.push(`${customId}: empty embedding`); continue; } byCustomId.set(customId, embedding); } if (errors.length > 0) throw new Error(`gemini batch ${batchName} failed: ${errors.join("; ")}`); if (remaining.size > 0) throw new Error(`gemini batch ${batchName} missing ${remaining.size} embedding responses`); } }); } //#endregion //#region src/memory/batch-error-utils.ts function extractBatchErrorMessage(lines) { const first = lines.find((line) => line.error?.message || line.response?.body?.error); return first?.error?.message ?? (typeof first?.response?.body?.error?.message === "string" ? first?.response?.body?.error?.message : void 0); } function formatUnavailableBatchError(err) { const message = err instanceof Error ? err.message : String(err); return message ? `error file unavailable: ${message}` : void 0; } //#endregion //#region src/memory/batch-http.ts async function postJsonWithRetry(params) { return await (await retryAsync(async () => { const res = await fetch(params.url, { method: "POST", headers: params.headers, body: JSON.stringify(params.body) }); if (!res.ok) { const text = await res.text(); const err = /* @__PURE__ */ new Error(`${params.errorPrefix}: ${res.status} ${text}`); err.status = res.status; throw err; } return res; }, { attempts: 3, minDelayMs: 300, maxDelayMs: 2e3, jitter: .2, shouldRetry: (err) => { const status = err.status; return status === 429 || typeof status === "number" && status >= 500; } })).json(); } //#endregion //#region src/memory/batch-output.ts function applyEmbeddingBatchOutputLine(params) { const customId = params.line.custom_id; if (!customId) return; params.remaining.delete(customId); const errorMessage = params.line.error?.message; if (errorMessage) { params.errors.push(`${customId}: ${errorMessage}`); return; } const response = params.line.response; if ((response?.status_code ?? 0) >= 400) { const messageFromObject = response?.body && typeof response.body === "object" ? response.body.error?.message : void 0; const messageFromString = typeof response?.body === "string" ? response.body : void 0; params.errors.push(`${customId}: ${messageFromObject ?? messageFromString ?? "unknown error"}`); return; } const embedding = (response?.body && typeof response.body === "object" ? response.body.data ?? [] : [])[0]?.embedding ?? []; if (embedding.length === 0) { params.errors.push(`${customId}: empty embedding`); return; } params.byCustomId.set(customId, embedding); } //#endregion //#region src/memory/batch-upload.ts async function uploadBatchJsonlFile(params) { const baseUrl = normalizeBatchBaseUrl(params.client); const jsonl = params.requests.map((request) => JSON.stringify(request)).join("\n"); const form = new FormData(); form.append("purpose", "batch"); form.append("file", new Blob([jsonl], { type: "application/jsonl" }), `memory-embeddings.${hashText(String(Date.now()))}.jsonl`); const fileRes = await fetch(`${baseUrl}/files`, { method: "POST", headers: buildBatchHeaders(params.client, { json: false }), body: form }); if (!fileRes.ok) { const text = await fileRes.text(); throw new Error(`${params.errorPrefix}: ${fileRes.status} ${text}`); } const filePayload = await fileRes.json(); if (!filePayload.id) throw new Error(`${params.errorPrefix}: missing file id`); return filePayload.id; } //#endregion //#region src/memory/batch-openai.ts const OPENAI_BATCH_ENDPOINT = "/v1/embeddings"; const OPENAI_BATCH_COMPLETION_WINDOW = "24h"; const OPENAI_BATCH_MAX_REQUESTS = 5e4; async function submitOpenAiBatch(params) { const baseUrl = normalizeBatchBaseUrl(params.openAi); const inputFileId = await uploadBatchJsonlFile({ client: params.openAi, requests: params.requests, errorPrefix: "openai batch file upload failed" }); return await postJsonWithRetry({ url: `${baseUrl}/batches`, headers: buildBatchHeaders(params.openAi, { json: true }), body: { input_file_id: inputFileId, endpoint: OPENAI_BATCH_ENDPOINT, completion_window: OPENAI_BATCH_COMPLETION_WINDOW, metadata: { source: "openclaw-memory", agent: params.agentId } }, errorPrefix: "openai batch create failed" }); } async function fetchOpenAiBatchStatus(params) { const baseUrl = normalizeBatchBaseUrl(params.openAi); const res = await fetch(`${baseUrl}/batches/${params.batchId}`, { headers: buildBatchHeaders(params.openAi, { json: true }) }); if (!res.ok) { const text = await res.text(); throw new Error(`openai batch status failed: ${res.status} ${text}`); } return await res.json(); } async function fetchOpenAiFileContent(params) { const baseUrl = normalizeBatchBaseUrl(params.openAi); const res = await fetch(`${baseUrl}/files/${params.fileId}/content`, { headers: buildBatchHeaders(params.openAi, { json: true }) }); if (!res.ok) { const text = await res.text(); throw new Error(`openai batch file content failed: ${res.status} ${text}`); } return await res.text(); } function parseOpenAiBatchOutput(text) { if (!text.trim()) return []; return text.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => JSON.parse(line)); } async function readOpenAiBatchError(params) { try { return extractBatchErrorMessage(parseOpenAiBatchOutput(await fetchOpenAiFileContent({ openAi: params.openAi, fileId: params.errorFileId }))); } catch (err) { return formatUnavailableBatchError(err); } } async function waitForOpenAiBatch(params) { const start = Date.now(); let current = params.initial; while (true) { const status = current ?? await fetchOpenAiBatchStatus({ openAi: params.openAi, batchId: params.batchId }); const state = status.status ?? "unknown"; if (state === "completed") { if (!status.output_file_id) throw new Error(`openai batch ${params.batchId} completed without output file`); return { o