mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 10:34:44 +00:00
fix(memory): load vector extension before publish
This commit is contained in:
@@ -3,7 +3,10 @@ import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { ensureMemoryIndexSchema } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import {
|
||||
ensureMemoryIndexSchema,
|
||||
loadSqliteVecExtension,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
cleanupAgedMemoryReindexTempFiles,
|
||||
@@ -37,7 +40,7 @@ describe("memory manager database publication", () => {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("removes a stale vector table when the shadow index has no vectors", () => {
|
||||
it("removes a stale vector table when the shadow index has no vectors", async () => {
|
||||
const targetPath = path.join(fixtureRoot, "target.sqlite");
|
||||
const sourcePath = path.join(fixtureRoot, "source.sqlite");
|
||||
const targetDb = new DatabaseSync(targetPath);
|
||||
@@ -49,7 +52,7 @@ describe("memory manager database publication", () => {
|
||||
targetDb.prepare("INSERT INTO chunks_vec (id, embedding) VALUES (?, ?)").run("stale", "[]");
|
||||
sourceDb.close();
|
||||
|
||||
publishMemoryDatabaseTables({
|
||||
await publishMemoryDatabaseTables({
|
||||
targetDb,
|
||||
sourcePath,
|
||||
metaKey: "memory_index_meta",
|
||||
@@ -69,7 +72,47 @@ describe("memory manager database publication", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a stale shadow publish after a concurrent live memory update", () => {
|
||||
it("loads sqlite-vec on the target before publishing a shadow vector table", async () => {
|
||||
const targetPath = path.join(fixtureRoot, "target.sqlite");
|
||||
const sourcePath = path.join(fixtureRoot, "source.sqlite");
|
||||
const targetDb = new DatabaseSync(targetPath, { allowExtension: true });
|
||||
const sourceDb = new DatabaseSync(sourcePath, { allowExtension: true });
|
||||
try {
|
||||
ensureTestMemorySchema(targetDb);
|
||||
ensureTestMemorySchema(sourceDb);
|
||||
const sourceVector = await loadSqliteVecExtension({ db: sourceDb });
|
||||
if (!sourceVector.ok) {
|
||||
return;
|
||||
}
|
||||
sourceDb.exec(`
|
||||
CREATE VIRTUAL TABLE chunks_vec USING vec0(
|
||||
id TEXT PRIMARY KEY,
|
||||
embedding FLOAT[3]
|
||||
)
|
||||
`);
|
||||
sourceDb
|
||||
.prepare("INSERT INTO chunks_vec (id, embedding) VALUES (?, ?)")
|
||||
.run("vector", JSON.stringify([0, 1, 0]));
|
||||
sourceDb.close();
|
||||
|
||||
await publishMemoryDatabaseTables({
|
||||
targetDb,
|
||||
sourcePath,
|
||||
metaKey: "memory_index_meta",
|
||||
expectedRevision: readMemoryDatabaseRevision(targetDb),
|
||||
vectorExtensionPath: sourceVector.extensionPath,
|
||||
});
|
||||
|
||||
expect(targetDb.prepare("SELECT id FROM chunks_vec").all()).toEqual([{ id: "vector" }]);
|
||||
} finally {
|
||||
try {
|
||||
sourceDb.close();
|
||||
} catch {}
|
||||
targetDb.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a stale shadow publish after a concurrent live memory update", async () => {
|
||||
const targetPath = path.join(fixtureRoot, "target.sqlite");
|
||||
const sourcePath = path.join(fixtureRoot, "source.sqlite");
|
||||
const targetDb = new DatabaseSync(targetPath);
|
||||
@@ -92,14 +135,14 @@ describe("memory manager database publication", () => {
|
||||
concurrentDb.close();
|
||||
concurrentDb = undefined;
|
||||
|
||||
expect(() =>
|
||||
await expect(
|
||||
publishMemoryDatabaseTables({
|
||||
targetDb,
|
||||
sourcePath,
|
||||
metaKey: "memory_index_meta",
|
||||
expectedRevision,
|
||||
}),
|
||||
).toThrow(/changed while full reindex was building/);
|
||||
).rejects.toThrow(/changed while full reindex was building/);
|
||||
expect(
|
||||
targetDb.prepare("SELECT hash FROM files WHERE path = ?").get("memory.md"),
|
||||
).toEqual({ hash: "newer" });
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
closeMemorySqliteWalMaintenance,
|
||||
configureMemorySqliteWalMaintenance,
|
||||
ensureDir,
|
||||
loadSqliteVecExtension,
|
||||
requireNodeSqlite,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
|
||||
import {
|
||||
@@ -67,6 +68,17 @@ function readTableSql(db: DatabaseSync, schema: string, tableName: string): stri
|
||||
return typeof row?.sql === "string" && row.sql.trim() ? row.sql : null;
|
||||
}
|
||||
|
||||
function hasSqliteVecExtension(db: DatabaseSync): boolean {
|
||||
try {
|
||||
const row = db.prepare("SELECT vec_version() AS version").get() as
|
||||
| { version?: unknown }
|
||||
| undefined;
|
||||
return typeof row?.version === "string" && row.version.trim().length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function readMemoryDatabaseRevision(db: DatabaseSync): number {
|
||||
const row = db
|
||||
.prepare("SELECT revision FROM memory_index_state WHERE id = ?")
|
||||
@@ -104,14 +116,30 @@ function replaceVirtualTable(params: {
|
||||
}
|
||||
|
||||
/** Publish a completed shadow memory index without replacing the shared agent database file. */
|
||||
export function publishMemoryDatabaseTables(params: {
|
||||
export async function publishMemoryDatabaseTables(params: {
|
||||
targetDb: DatabaseSync;
|
||||
sourcePath: string;
|
||||
metaKey: string;
|
||||
expectedRevision: number;
|
||||
}): void {
|
||||
vectorExtensionPath?: string;
|
||||
}): Promise<void> {
|
||||
params.targetDb.prepare(`ATTACH DATABASE ? AS ${MEMORY_REINDEX_SCHEMA}`).run(params.sourcePath);
|
||||
try {
|
||||
if (
|
||||
tableExists(params.targetDb, MEMORY_REINDEX_SCHEMA, "chunks_vec") &&
|
||||
!hasSqliteVecExtension(params.targetDb)
|
||||
) {
|
||||
const loaded = await loadSqliteVecExtension({
|
||||
db: params.targetDb,
|
||||
extensionPath: params.vectorExtensionPath,
|
||||
});
|
||||
if (!loaded.ok) {
|
||||
throw new Error(
|
||||
`Failed to load sqlite-vec before publishing the full memory reindex: ` +
|
||||
`${loaded.error ?? "unknown sqlite-vec load error"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
runSqliteImmediateTransactionSync(params.targetDb, () => {
|
||||
const liveRevision = readMemoryDatabaseRevision(params.targetDb);
|
||||
if (liveRevision !== params.expectedRevision) {
|
||||
|
||||
@@ -2530,11 +2530,12 @@ export abstract class MemoryManagerSyncOps {
|
||||
|
||||
closeMemoryDatabase(tempDb);
|
||||
tempDbClosed = true;
|
||||
publishMemoryDatabaseTables({
|
||||
await publishMemoryDatabaseTables({
|
||||
targetDb: originalDb,
|
||||
sourcePath: tempDbPath,
|
||||
metaKey: META_KEY,
|
||||
expectedRevision: originalRevision,
|
||||
vectorExtensionPath: this.vector.extensionPath,
|
||||
});
|
||||
|
||||
this.db = originalDb;
|
||||
|
||||
Reference in New Issue
Block a user