openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
83 lines (82 loc) • 4.66 kB
JavaScript
import { r as truncateUtf16Safe } from "./utf16-slice-D_ngcYKd.js";
import "./memory-core-host-engine-foundation-BZwrR3xr.js";
//#region extensions/memory-core/src/memory/vector-blob.ts
const vectorToBlob = (embedding) => Buffer.from(new Float32Array(embedding).buffer);
//#endregion
//#region extensions/memory-core/src/memory/manager-search-knn.ts
const VECTOR_KNN_OVERSAMPLE_FACTOR = 8;
const MAX_VECTOR_KNN_K = 4096;
const SQL_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/u;
const SOURCE_FILTER_RE = /^(?:| AND c\.source IN \(\?(?:, \?)*\))$/u;
function readCount(row) {
if (!row || typeof row !== "object") return 0;
const count = Reflect.get(row, "count");
if (typeof count === "bigint") return Number(count);
if (typeof count === "number") return count;
return 0;
}
function isVectorKnnRow(value) {
if (!value || typeof value !== "object") return false;
const id = Reflect.get(value, "id");
const path = Reflect.get(value, "path");
const startLine = Reflect.get(value, "start_line");
const endLine = Reflect.get(value, "end_line");
const text = Reflect.get(value, "text");
const source = Reflect.get(value, "source");
const dist = Reflect.get(value, "dist");
return typeof id === "string" && typeof path === "string" && typeof startLine === "number" && typeof endLine === "number" && typeof text === "string" && (source === "memory" || source === "sessions") && typeof dist === "number" && Number.isFinite(dist);
}
function buildModelFilter(column, models) {
return models.length === 1 ? `${column} = ?` : `${column} IN (${models.map(() => "?").join(", ")})`;
}
function validateRequest(request) {
if (!SQL_IDENTIFIER_RE.test(request.vectorTable)) throw new Error("invalid memory vector table identifier");
if (request.providerModels.length === 0 || request.providerModels.some((model) => typeof model !== "string" || model.length === 0)) throw new Error("memory vector KNN requires at least one provider model");
if (!SOURCE_FILTER_RE.test(request.sourceFilter.sql)) throw new Error("invalid memory vector source filter");
if ((request.sourceFilter.sql.match(/\?/gu)?.length ?? 0) !== request.sourceFilter.params.length) throw new Error("memory vector source filter parameter mismatch");
if (!Number.isSafeInteger(request.limit) || request.limit <= 0) throw new Error("invalid memory vector KNN limit");
if (!Number.isSafeInteger(request.snippetMaxChars) || request.snippetMaxChars <= 0) throw new Error("invalid memory vector KNN snippet limit");
}
/**
* Execute the complete synchronous sqlite-vec KNN/count sequence.
*
* This function must run outside the Gateway event loop for file-backed
* indexes. It remains separately testable so the worker and query semantics do
* not diverge.
*/
function runVectorKnnQuery(db, request) {
validateRequest(request);
const vectorModelFilter = buildModelFilter("c.model", request.providerModels);
const qBlob = vectorToBlob(request.queryVec);
const runVectorQuery = (candidateLimit) => {
return db.prepare(`SELECT c.id, c.path, c.start_line, c.end_line, c.text,
c.source,
vec_distance_cosine(v.embedding, ?) AS dist
FROM ${request.vectorTable} v\n JOIN memory_index_chunks c ON c.id = v.id\n WHERE v.embedding MATCH ? AND k = ? AND ${vectorModelFilter}${request.sourceFilter.sql}\n ORDER BY dist ASC\n LIMIT ?`).all(qBlob, qBlob, candidateLimit, ...request.providerModels, ...request.sourceFilter.params, request.limit).map((row) => {
if (!isVectorKnnRow(row)) throw new Error("memory vector KNN query returned an invalid row");
row.text = truncateUtf16Safe(row.text, request.snippetMaxChars);
return row;
});
};
const candidateLimit = Math.min(request.limit * VECTOR_KNN_OVERSAMPLE_FACTOR, MAX_VECTOR_KNN_K);
let rows = runVectorQuery(candidateLimit);
if (rows.length < request.limit) {
const matchingChunkCount = readCount(db.prepare(`SELECT COUNT(*) AS count FROM memory_index_chunks c WHERE ${vectorModelFilter}${request.sourceFilter.sql}`).get(...request.providerModels, ...request.sourceFilter.params));
if (matchingChunkCount > rows.length) {
const vectorCount = readCount(db.prepare(`SELECT COUNT(*) AS count FROM ${request.vectorTable}`).get());
const widenedLimit = Math.min(vectorCount, MAX_VECTOR_KNN_K);
if (widenedLimit > candidateLimit) rows = runVectorQuery(widenedLimit);
const requiredMatches = Math.min(request.limit, matchingChunkCount);
if (vectorCount > MAX_VECTOR_KNN_K && rows.length < requiredMatches) return {
rows: [],
fallbackScanRequired: true
};
}
}
return {
rows,
fallbackScanRequired: false
};
}
//#endregion
export { runVectorKnnQuery as n, vectorToBlob as r, isVectorKnnRow as t };