@gguf/claw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,346 lines (1,326 loc) • 131 kB
JavaScript
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