Skip to content

Commit 494af2d

Browse files
[AI] fix(memory): narrow providerKey auto-fix to stale FTS-only key only
Restrict the auto-fix predicate to only rewrite meta.providerKey when the stored key is the exact FTS-only sentinel (SHA256({provider:'none',model:'fts-only'})), which CLI writes before provider initialization. This preserves the reindex safeguard for genuine same-provider/model setting changes (dimensions, baseUrl, headers) that must still trigger reindexing. Add negative tests: - runtime dimensions differ → still mismatched - runtime baseUrl differs → still mismatched - provider id differs → still mismatched Refs #91902
1 parent 74aece3 commit 494af2d

3 files changed

Lines changed: 89 additions & 14 deletions

File tree

extensions/memory-core/src/memory/manager.providerkey-auto-fix.test.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import crypto from "node:crypto";
12
import fs from "node:fs/promises";
23
import os from "node:os";
34
import path from "node:path";
45
import { DatabaseSync } from "node:sqlite";
5-
import crypto from "node:crypto";
66
import type { OpenClawConfig } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
77
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
88
import { closeAllMemorySearchManagers, getMemorySearchManager } from "./index.js";
@@ -16,11 +16,35 @@ const MOCK_CACHE_KEY_DATA = {
1616
dimensions: 1024,
1717
};
1818

19+
const MOCK_CACHE_KEY_DATA_DIFFERENT_DIMS = {
20+
provider: "openai-compatible",
21+
baseUrl: "https://dashscope.example.com/v1",
22+
model: "text-embedding-v4",
23+
dimensions: 3072,
24+
};
25+
26+
const MOCK_CACHE_KEY_DATA_DIFFERENT_BASE_URL = {
27+
provider: "openai-compatible",
28+
baseUrl: "https://different-endpoint.example.com/v1",
29+
model: "text-embedding-v4",
30+
dimensions: 1024,
31+
};
32+
1933
const STALE_PROVIDER_KEY = crypto
2034
.createHash("sha256")
2135
.update(JSON.stringify({ provider: "none", model: "fts-only" }))
2236
.digest("hex");
2337

38+
const DIFFERENT_DIMS_PROVIDER_KEY = crypto
39+
.createHash("sha256")
40+
.update(JSON.stringify(MOCK_CACHE_KEY_DATA_DIFFERENT_DIMS))
41+
.digest("hex");
42+
43+
const DIFFERENT_BASE_URL_PROVIDER_KEY = crypto
44+
.createHash("sha256")
45+
.update(JSON.stringify(MOCK_CACHE_KEY_DATA_DIFFERENT_BASE_URL))
46+
.digest("hex");
47+
2448
const createEmbeddingProviderMock = vi.hoisted(() =>
2549
vi.fn(async () => ({
2650
requestedProvider: "openai-compatible",
@@ -43,9 +67,10 @@ vi.mock("./embeddings.js", () => ({
4367
resolveEmbeddingProviderAdapterTransport: (providerId: string) =>
4468
providerId === "local" ? "local" : "remote",
4569
resolveEmbeddingProviderFallbackModel: () => "fts-only",
70+
resolveEmbeddingProviderIndexIdentity: () => undefined,
4671
}));
4772

48-
describe("memory manager auto-fixes providerKey mismatch from CLI index --force", () => {
73+
describe("memory manager auto-fixes stale FTS-only providerKey from CLI index --force", () => {
4974
let fixtureRoot = "";
5075
let caseId = 0;
5176
let workspaceDir = "";
@@ -113,9 +138,9 @@ describe("memory manager auto-fixes providerKey mismatch from CLI index --force"
113138

114139
function overwriteProviderKeyInMeta(providerKey: string): void {
115140
const db = new DatabaseSync(indexPath);
116-
const row = db
117-
.prepare(`SELECT value FROM meta WHERE key = ?`)
118-
.get("memory_index_meta_v1") as { value: string } | undefined;
141+
const row = db.prepare(`SELECT value FROM meta WHERE key = ?`).get("memory_index_meta_v1") as
142+
| { value: string }
143+
| undefined;
119144
if (!row?.value) {
120145
db.close();
121146
throw new Error("no meta row found");
@@ -129,7 +154,7 @@ describe("memory manager auto-fixes providerKey mismatch from CLI index --force"
129154
db.close();
130155
}
131156

132-
it("auto-fixes stale providerKey and allows search after provider init", async () => {
157+
it("auto-fixes stale FTS-only providerKey written by CLI before provider init", async () => {
133158
const firstManager = await createManager();
134159
await firstManager.sync({ reason: "cli", force: true });
135160
expect(indexIdentityStatus(firstManager)).toBe("valid");
@@ -142,15 +167,13 @@ describe("memory manager auto-fixes providerKey mismatch from CLI index --force"
142167

143168
const reopenedManager = await createManager();
144169

145-
expect(indexIdentityStatus(reopenedManager)).toBe("valid");
146-
147170
const results = await reopenedManager.search("Test topic");
148171

149172
expect(indexIdentityStatus(reopenedManager)).toBe("valid");
150173
expect(results.length).toBeGreaterThan(0);
151174
});
152175

153-
it("does not auto-fix providerKey mismatch when provider id also differs", async () => {
176+
it("does not auto-fix when provider id also differs", async () => {
154177
const firstManager = await createManager();
155178
await firstManager.sync({ reason: "cli", force: true });
156179

@@ -159,9 +182,9 @@ describe("memory manager auto-fixes providerKey mismatch from CLI index --force"
159182
await closeAllMemorySearchManagers();
160183

161184
const db = new DatabaseSync(indexPath);
162-
const row = db
163-
.prepare(`SELECT value FROM meta WHERE key = ?`)
164-
.get("memory_index_meta_v1") as { value: string } | undefined;
185+
const row = db.prepare(`SELECT value FROM meta WHERE key = ?`).get("memory_index_meta_v1") as
186+
| { value: string }
187+
| undefined;
165188
if (!row?.value) {
166189
db.close();
167190
throw new Error("no meta row found");
@@ -182,4 +205,40 @@ describe("memory manager auto-fixes providerKey mismatch from CLI index --force"
182205
expect(indexIdentityStatus(reopenedManager)).toBe("mismatched");
183206
expect(results.length).toBe(0);
184207
});
208+
209+
it("does not auto-fix when runtime dimensions differ from index", async () => {
210+
const firstManager = await createManager();
211+
await firstManager.sync({ reason: "cli", force: true });
212+
213+
await manager!.close();
214+
manager = null;
215+
await closeAllMemorySearchManagers();
216+
217+
overwriteProviderKeyInMeta(DIFFERENT_DIMS_PROVIDER_KEY);
218+
219+
const reopenedManager = await createManager();
220+
221+
const results = await reopenedManager.search("Test topic");
222+
223+
expect(indexIdentityStatus(reopenedManager)).toBe("mismatched");
224+
expect(results.length).toBe(0);
225+
});
226+
227+
it("does not auto-fix when runtime baseUrl differs from index", async () => {
228+
const firstManager = await createManager();
229+
await firstManager.sync({ reason: "cli", force: true });
230+
231+
await manager!.close();
232+
manager = null;
233+
await closeAllMemorySearchManagers();
234+
235+
overwriteProviderKeyInMeta(DIFFERENT_BASE_URL_PROVIDER_KEY);
236+
237+
const reopenedManager = await createManager();
238+
239+
const results = await reopenedManager.search("Test topic");
240+
241+
expect(indexIdentityStatus(reopenedManager)).toBe("mismatched");
242+
expect(results.length).toBe(0);
243+
});
185244
});

extensions/memory-core/src/memory/manager.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
} from "./manager-cache.js";
4444
import { closeMemoryDatabase } from "./manager-db.js";
4545
import { MemoryManagerEmbeddingOps } from "./manager-embedding-ops.js";
46+
import { hashText } from "openclaw/plugin-sdk/memory-core-host-engine-storage";
4647
import { isLocalEmbeddingWorkerFailure } from "./manager-local-worker-errors.js";
4748
import {
4849
createDegradedMemoryProviderLifecycle,
@@ -562,6 +563,17 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
562563
}
563564
}
564565

566+
private static FTS_ONLY_PROVIDER_KEY: string | undefined;
567+
568+
private static getFtsOnlyProviderKey(): string {
569+
if (!MemoryIndexManager.FTS_ONLY_PROVIDER_KEY) {
570+
MemoryIndexManager.FTS_ONLY_PROVIDER_KEY = hashText(
571+
JSON.stringify({ provider: "none", model: "fts-only" }),
572+
);
573+
}
574+
return MemoryIndexManager.FTS_ONLY_PROVIDER_KEY;
575+
}
576+
565577
private refreshIndexIdentityDirty(params?: { providerKeyKnown?: boolean }) {
566578
const provider =
567579
this.settings.provider === "none"
@@ -582,7 +594,11 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
582594
this.providerKey
583595
) {
584596
const meta = this.readMeta();
585-
if (meta && meta.providerKey !== this.providerKey) {
597+
if (
598+
meta &&
599+
meta.providerKey === MemoryIndexManager.getFtsOnlyProviderKey() &&
600+
meta.providerKey !== this.providerKey
601+
) {
586602
meta.providerKey = this.providerKey;
587603
this.writeMeta(meta);
588604
const repaired = this.resolveCurrentIndexIdentityState({

src/scripts/test-projects.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,8 +702,8 @@ describe("test-projects args", () => {
702702
includePatterns: [
703703
"extensions/memory-core/src/memory/index.test.ts",
704704
"extensions/memory-core/src/memory/manager.fts-only-reindex.test.ts",
705-
"extensions/memory-core/src/memory/manager.reindex-recovery.test.ts",
706705
"extensions/memory-core/src/memory/manager.providerkey-auto-fix.test.ts",
706+
"extensions/memory-core/src/memory/manager.reindex-recovery.test.ts",
707707
"extensions/memory-core/src/memory/manager.self-heal-missing-identity.test.ts",
708708
],
709709
watchMode: false,

0 commit comments

Comments
 (0)