Skip to content

Commit d46dc39

Browse files
committed
fix(memory): rebuild missing index metadata safely
Gateway/background sync now repairs missing memory index metadata with the existing full reindex path when the configured embedding provider is available, while preserving dirty/paused state instead of downgrading semantic chunks when embeddings are unavailable. Fixes #90338
1 parent e3ef136 commit d46dc39

2 files changed

Lines changed: 62 additions & 1 deletion

File tree

extensions/memory-core/src/memory/index.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,12 +549,13 @@ describe("memory index", () => {
549549
}
550550
});
551551

552-
it("does not search stale rows when index metadata is missing", async () => {
552+
it("rebuilds missing metadata with existing chunks on gateway sync", async () => {
553553
const dbPath = path.join(workspaceDir, "index-missing-meta-cutover.sqlite");
554554
const cfg = createCfg({
555555
storePath: dbPath,
556556
hybrid: { enabled: true, vectorWeight: 0.5, textWeight: 0.5 },
557557
});
558+
await fs.writeFile(path.join(memoryDir, "2026-01-13.md"), "# Log\nBeta memory line.");
558559
const oldManager = await getFreshManager(cfg);
559560
await oldManager.sync({ reason: "test", force: true });
560561
await oldManager.close?.();
@@ -580,6 +581,19 @@ describe("memory index", () => {
580581
status: "missing",
581582
reason: "index metadata is missing",
582583
});
584+
585+
vi.stubEnv("OPENCLAW_TEST_MEMORY_UNSAFE_REINDEX", "0");
586+
await nextManager.sync({ reason: "test" });
587+
588+
expect(nextManager.status().dirty).toBe(false);
589+
expect(nextManager.status().custom?.indexIdentity).toEqual({ status: "valid" });
590+
const repairedAlphaResults = await nextManager.search("alpha");
591+
expect(
592+
repairedAlphaResults.some((result) => result.path.endsWith("memory/2026-01-12.md")),
593+
).toBe(false);
594+
const repairedResults = await nextManager.search("beta");
595+
expect(repairedResults.length).toBeGreaterThan(0);
596+
expect(repairedResults[0]?.path).toContain("memory/2026-01-13.md");
583597
} finally {
584598
await nextManager.close?.();
585599
}
@@ -611,6 +625,46 @@ describe("memory index", () => {
611625
}
612626
});
613627

628+
it("does not rebuild missing semantic metadata when embeddings are unavailable", async () => {
629+
const dbPath = path.join(workspaceDir, "index-missing-meta-provider-unavailable.sqlite");
630+
const oldCfg = createCfg({
631+
storePath: dbPath,
632+
model: "semantic-embed",
633+
hybrid: { enabled: true, vectorWeight: 0.5, textWeight: 0.5 },
634+
});
635+
const oldManager = await getFreshManager(oldCfg);
636+
await oldManager.sync({ reason: "test", force: true });
637+
await oldManager.close?.();
638+
639+
forceNoProvider = true;
640+
const nextManager = await getFreshManager(oldCfg);
641+
try {
642+
const db = (
643+
nextManager as unknown as {
644+
db: {
645+
exec: (sql: string) => void;
646+
prepare: (sql: string) => {
647+
get: () => { model?: string } | undefined;
648+
};
649+
};
650+
}
651+
).db;
652+
db.exec(`DELETE FROM meta WHERE key = 'memory_index_meta_v1'`);
653+
654+
await nextManager.sync({ reason: "test" });
655+
656+
expect(nextManager.status().dirty).toBe(true);
657+
expect(nextManager.status().custom?.indexIdentity).toEqual({
658+
status: "missing",
659+
reason: "index metadata is missing",
660+
});
661+
const row = db.prepare("SELECT model FROM chunks LIMIT 1").get();
662+
expect(row?.model).toBe("semantic-embed");
663+
} finally {
664+
await nextManager.close?.();
665+
}
666+
});
667+
614668
it("clears dirty after sessions-only identity reindex", async () => {
615669
try {
616670
vi.stubEnv("OPENCLAW_STATE_DIR", path.join(workspaceDir, ".state-sessions-only-reindex"));

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1842,11 +1842,18 @@ export abstract class MemoryManagerSyncOps {
18421842
});
18431843
const hasIndexedChunks = this.hasIndexedChunks();
18441844
const needsInitialIndex = indexIdentity.status !== "valid" && !hasIndexedChunks;
1845+
// Missing metadata cannot prove whether existing chunks were semantic.
1846+
// Wait for the configured provider before replacing them with a rebuilt index.
1847+
const canRebuildMissingIdentity =
1848+
this.provider !== null || !this.settings.provider || this.settings.provider === "none";
1849+
const needsMissingIdentityReindex =
1850+
indexIdentity.status === "missing" && !hasTargetSessionFiles && canRebuildMissingIdentity;
18451851
const needsExplicitIdentityReindex =
18461852
params?.reason === "cli" && indexIdentity.status !== "valid" && !hasTargetSessionFiles;
18471853
const needsFullReindex =
18481854
(params?.force && !hasTargetSessionFiles) ||
18491855
needsInitialIndex ||
1856+
needsMissingIdentityReindex ||
18501857
needsExplicitIdentityReindex;
18511858
if (indexIdentity.status !== "valid" && !needsFullReindex) {
18521859
this.dirty = true;

0 commit comments

Comments
 (0)