Skip to content

Commit a38c2c2

Browse files
committed
fix(memory): split vector store readiness
1 parent 3617778 commit a38c2c2

16 files changed

Lines changed: 276 additions & 36 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
2727
### Fixes
2828

2929
- Google Meet: route stateful CLI session commands through the gateway-owned runtime so joined realtime sessions survive after the starting CLI process exits. Fixes #76344. Thanks @coltonharris-wq.
30+
- Memory/status: split builtin sqlite-vec store readiness from embedding-provider readiness in `memory status --deep` and `openclaw status`, so local vector-store failures no longer look like provider failures and provider failures no longer hide a healthy local vector store.
3031
- Memory/status: keep plain `openclaw memory status` and `openclaw memory status --json` on the cheap read-only path by reserving vector and embedding provider probes for `--deep` or `--index`. Fixes #76769. Thanks @daruire.
3132
- Telegram: suppress stale same-session replies when a newer accepted message arrives before an older in-flight Telegram dispatch finalizes. Fixes #76642. Thanks @chinar-amrutkar.
3233
- Control UI/Sessions: avoid full `sessions.list` reloads for chat-turn `sessions.changed` payloads, so large session stores no longer add multi-second delays while chat responses are being delivered. (#76676) Thanks @VACInc.

docs/cli/memory.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ openclaw memory index --agent main --verbose
5151

5252
`memory status`:
5353

54-
- `--deep`: probe vector + embedding availability. Plain `memory status` stays fast and does not run a live embedding ping. QMD lexical `searchMode: "search"` skips semantic vector probes and embedding maintenance even with `--deep`.
54+
- `--deep`: probe local vector-store readiness, embedding-provider readiness, and semantic vector-search readiness. Plain `memory status` stays fast and does not run live embedding or provider discovery work; unknown vector-store or semantic-vector state means it was not probed in that command. QMD lexical `searchMode: "search"` skips semantic vector probes and embedding maintenance even with `--deep`.
5555
- `--index`: run a reindex if the store is dirty (implies `--deep`).
5656
- `--fix`: repair stale recall locks and normalize promotion metadata.
5757
- `--json`: print JSON output.

docs/concepts/memory-builtin.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ when `memorySearch.local.modelPath` points to an existing local file.
127127
may miss changes in rare edge cases.
128128

129129
**sqlite-vec not loading?** OpenClaw falls back to in-process cosine similarity
130-
automatically. Check logs for the specific load error.
130+
automatically. `openclaw memory status --deep` reports the local vector store
131+
separately from the embedding provider, so `Vector store: unavailable` points
132+
at sqlite-vec loading while `Embeddings: unavailable` points at provider/auth
133+
or model readiness. Check logs for the specific load error.
131134

132135
## Configuration
133136

extensions/memory-core/src/cli.runtime.ts

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -676,14 +676,30 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
676676
let indexError: string | undefined;
677677
const syncFn = manager.sync ? manager.sync.bind(manager) : undefined;
678678
if (deep) {
679-
await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => {
680-
progress.setLabel("Probing vector…");
681-
await manager.probeVectorAvailability();
682-
progress.tick();
683-
progress.setLabel("Probing embeddings…");
684-
embeddingProbe = await manager.probeEmbeddingAvailability();
685-
progress.tick();
686-
});
679+
const initialStatus = manager.status();
680+
const hasVectorStoreProbe =
681+
initialStatus.backend === "builtin" &&
682+
typeof manager.probeVectorStoreAvailability === "function";
683+
await withProgress(
684+
{ label: "Checking memory…", total: hasVectorStoreProbe ? 3 : 2 },
685+
async (progress) => {
686+
progress.setLabel(hasVectorStoreProbe ? "Probing vector store…" : "Probing vectors…");
687+
if (hasVectorStoreProbe) {
688+
await manager.probeVectorStoreAvailability?.();
689+
} else {
690+
await manager.probeVectorAvailability();
691+
}
692+
progress.tick();
693+
progress.setLabel("Probing embeddings…");
694+
embeddingProbe = await manager.probeEmbeddingAvailability();
695+
progress.tick();
696+
if (hasVectorStoreProbe) {
697+
progress.setLabel("Checking semantic vectors…");
698+
await manager.probeVectorAvailability();
699+
progress.tick();
700+
}
701+
},
702+
);
687703
if (opts.index && syncFn) {
688704
await withProgressTotals(
689705
{
@@ -856,20 +872,31 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) {
856872
lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`);
857873
}
858874
if (status.vector) {
859-
const vectorState = status.vector.enabled
860-
? status.vector.available === undefined
861-
? "unknown"
862-
: status.vector.available
863-
? "ready"
864-
: "unavailable"
865-
: "disabled";
866-
const vectorColor =
867-
vectorState === "ready"
868-
? theme.success
869-
: vectorState === "unavailable"
870-
? theme.warn
871-
: theme.muted;
872-
lines.push(`${label("Vector")} ${colorize(rich, vectorColor, vectorState)}`);
875+
const formatVectorState = (available: boolean | undefined) =>
876+
status.vector?.enabled
877+
? available === undefined
878+
? "unknown"
879+
: available
880+
? "ready"
881+
: "unavailable"
882+
: "disabled";
883+
const formatVectorLine = (lineLabel: string, state: string) => {
884+
const vectorColor =
885+
state === "ready" ? theme.success : state === "unavailable" ? theme.warn : theme.muted;
886+
lines.push(`${label(lineLabel)} ${colorize(rich, vectorColor, state)}`);
887+
};
888+
if (status.backend === "builtin") {
889+
const storeState = formatVectorState(status.vector.storeAvailable);
890+
formatVectorLine("Vector store", storeState);
891+
if (status.vector.semanticAvailable !== undefined) {
892+
formatVectorLine("Semantic vectors", formatVectorState(status.vector.semanticAvailable));
893+
}
894+
} else {
895+
const vectorState = formatVectorState(
896+
status.vector.semanticAvailable ?? status.vector.available,
897+
);
898+
formatVectorLine("Vector", vectorState);
899+
}
873900
if (status.vector.dims) {
874901
lines.push(`${label("Vector dims")} ${info(String(status.vector.dims))}`);
875902
}
@@ -1117,7 +1144,8 @@ export async function runMemoryIndex(opts: MemoryCommandOptions) {
11171144
}
11181145
const postIndexStatus = manager.status();
11191146
const vectorEnabled = postIndexStatus.vector?.enabled ?? false;
1120-
const vectorAvailable = postIndexStatus.vector?.available;
1147+
const vectorAvailable =
1148+
postIndexStatus.vector?.storeAvailable ?? postIndexStatus.vector?.available;
11211149
const vectorLoadErr = postIndexStatus.vector?.loadError;
11221150
if (vectorEnabled && vectorAvailable === false) {
11231151
const errDetail = vectorLoadErr ? `: ${vectorLoadErr}` : "";

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

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ describe("memory cli", () => {
105105

106106
function makeMemoryStatus(overrides: Record<string, unknown> = {}) {
107107
return {
108+
backend: "builtin",
108109
files: 0,
109110
chunks: 0,
110111
dirty: false,
@@ -113,7 +114,7 @@ describe("memory cli", () => {
113114
provider: "openai",
114115
model: "text-embedding-3-small",
115116
requestedProvider: "openai",
116-
vector: { enabled: true, available: true },
117+
vector: { enabled: true, storeAvailable: true, semanticAvailable: true, available: true },
117118
...overrides,
118119
};
119120
}
@@ -226,6 +227,8 @@ describe("memory cli", () => {
226227
fts: { enabled: true, available: true },
227228
vector: {
228229
enabled: true,
230+
storeAvailable: true,
231+
semanticAvailable: true,
229232
available: true,
230233
extensionPath: "/opt/sqlite-vec.dylib",
231234
dims: 1024,
@@ -238,7 +241,8 @@ describe("memory cli", () => {
238241
await runMemoryCli(["status"]);
239242

240243
expect(probeVectorAvailability).not.toHaveBeenCalled();
241-
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready"));
244+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: ready"));
245+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Semantic vectors: ready"));
242246
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024"));
243247
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector path: /opt/sqlite-vec.dylib"));
244248
expect(log).toHaveBeenCalledWith(expect.stringContaining("FTS: ready"));
@@ -274,7 +278,7 @@ describe("memory cli", () => {
274278
expect(probeVectorAvailability).not.toHaveBeenCalled();
275279
expect(probeEmbeddingAvailability).not.toHaveBeenCalled();
276280
expect(log).toHaveBeenCalledWith(expect.stringContaining("Provider: auto"));
277-
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unknown"));
281+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: unknown"));
278282
expect(close).toHaveBeenCalled();
279283
});
280284

@@ -350,6 +354,8 @@ describe("memory cli", () => {
350354
dirty: true,
351355
vector: {
352356
enabled: true,
357+
storeAvailable: false,
358+
semanticAvailable: false,
353359
available: false,
354360
loadError: "load failed",
355361
},
@@ -360,16 +366,19 @@ describe("memory cli", () => {
360366
const log = spyRuntimeLogs(defaultRuntime);
361367
await runMemoryCli(["status", "--agent", "main"]);
362368

363-
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unavailable"));
369+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: unavailable"));
370+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Semantic vectors: unavailable"));
364371
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector error: load failed"));
365372
expect(close).toHaveBeenCalled();
366373
});
367374

368375
it("prints embeddings status when deep", async () => {
369376
const close = vi.fn(async () => {});
377+
const probeVectorStoreAvailability = vi.fn(async () => true);
370378
const probeVectorAvailability = vi.fn(async () => true);
371379
const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true }));
372380
mockManager({
381+
probeVectorStoreAvailability,
373382
probeVectorAvailability,
374383
probeEmbeddingAvailability,
375384
status: () => makeMemoryStatus({ files: 1, chunks: 1 }),
@@ -379,12 +388,89 @@ describe("memory cli", () => {
379388
const log = spyRuntimeLogs(defaultRuntime);
380389
await runMemoryCli(["status", "--deep"]);
381390

391+
expect(probeVectorStoreAvailability).toHaveBeenCalled();
382392
expect(probeVectorAvailability).toHaveBeenCalled();
383393
expect(probeEmbeddingAvailability).toHaveBeenCalled();
384394
expect(log).toHaveBeenCalledWith(expect.stringContaining("Embeddings: ready"));
385395
expect(close).toHaveBeenCalled();
386396
});
387397

398+
it("prints vector store separately from embedding readiness when deep", async () => {
399+
const close = vi.fn(async () => {});
400+
const probeVectorStoreAvailability = vi.fn(async () => true);
401+
const probeVectorAvailability = vi.fn(async () => false);
402+
const probeEmbeddingAvailability = vi.fn(async () => ({
403+
ok: false,
404+
error: "No embedding provider available",
405+
}));
406+
mockManager({
407+
probeVectorStoreAvailability,
408+
probeVectorAvailability,
409+
probeEmbeddingAvailability,
410+
status: () =>
411+
makeMemoryStatus({
412+
provider: "none",
413+
requestedProvider: "auto",
414+
vector: {
415+
enabled: true,
416+
storeAvailable: true,
417+
semanticAvailable: false,
418+
available: false,
419+
},
420+
}),
421+
close,
422+
});
423+
424+
const log = spyRuntimeLogs(defaultRuntime);
425+
await runMemoryCli(["status", "--deep"]);
426+
427+
expect(probeVectorStoreAvailability).toHaveBeenCalled();
428+
expect(probeEmbeddingAvailability).toHaveBeenCalled();
429+
expect(probeVectorAvailability).toHaveBeenCalled();
430+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: ready"));
431+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Semantic vectors: unavailable"));
432+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Embeddings: unavailable"));
433+
expect(log).toHaveBeenCalledWith(
434+
expect.stringContaining("Embeddings error: No embedding provider available"),
435+
);
436+
expect(close).toHaveBeenCalled();
437+
});
438+
439+
it("keeps non-builtin deep status on the semantic vector probe", async () => {
440+
const close = vi.fn(async () => {});
441+
const probeVectorStoreAvailability = vi.fn(async () => true);
442+
const probeVectorAvailability = vi.fn(async () => true);
443+
const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true }));
444+
mockManager({
445+
probeVectorStoreAvailability,
446+
probeVectorAvailability,
447+
probeEmbeddingAvailability,
448+
status: () =>
449+
makeMemoryStatus({
450+
backend: "qmd",
451+
provider: "qmd",
452+
model: "qmd",
453+
requestedProvider: "qmd",
454+
vector: {
455+
enabled: true,
456+
semanticAvailable: true,
457+
available: true,
458+
},
459+
}),
460+
close,
461+
});
462+
463+
const log = spyRuntimeLogs(defaultRuntime);
464+
await runMemoryCli(["status", "--deep"]);
465+
466+
expect(probeVectorStoreAvailability).not.toHaveBeenCalled();
467+
expect(probeVectorAvailability).toHaveBeenCalled();
468+
expect(probeEmbeddingAvailability).toHaveBeenCalled();
469+
expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready"));
470+
expect(log).not.toHaveBeenCalledWith(expect.stringContaining("Vector store:"));
471+
expect(close).toHaveBeenCalled();
472+
});
473+
388474
it("prints recall-store audit details during status", async () => {
389475
await withTempWorkspace(async (workspaceDir) => {
390476
await recordShortTermRecalls({
@@ -578,9 +664,11 @@ describe("memory cli", () => {
578664
it("reindexes on status --index", async () => {
579665
const close = vi.fn(async () => {});
580666
const sync = vi.fn(async () => {});
667+
const probeVectorStoreAvailability = vi.fn(async () => true);
581668
const probeVectorAvailability = vi.fn(async () => true);
582669
const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true }));
583670
mockManager({
671+
probeVectorStoreAvailability,
584672
probeVectorAvailability,
585673
probeEmbeddingAvailability,
586674
sync,
@@ -592,6 +680,7 @@ describe("memory cli", () => {
592680
await runMemoryCli(["status", "--index"]);
593681

594682
expectCliSync(sync);
683+
expect(probeVectorStoreAvailability).toHaveBeenCalled();
595684
expect(probeVectorAvailability).toHaveBeenCalled();
596685
expect(probeEmbeddingAvailability).toHaveBeenCalled();
597686
expect(getMemorySearchManager).toHaveBeenCalledWith({

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,9 +406,29 @@ describe("memory index", () => {
406406
const status = manager.status();
407407
expect(status.vector?.enabled).toBe(true);
408408
expect(typeof status.vector?.available).toBe("boolean");
409+
expect(status.vector?.storeAvailable).toBe(available);
410+
expect(status.vector?.semanticAvailable).toBe(available);
409411
expect(status.vector?.available).toBe(available);
410412
});
411413

414+
it("probes sqlite vector store availability without initializing embeddings", async () => {
415+
forceNoProvider = true;
416+
const cfg = createCfg({
417+
storePath: path.join(workspaceDir, "index-vector-store-only.sqlite"),
418+
vectorEnabled: true,
419+
});
420+
const manager = await getPersistentManager(cfg);
421+
422+
const available = await manager.probeVectorStoreAvailability?.();
423+
const status = manager.status();
424+
425+
expect(providerCalls).toEqual([]);
426+
expect(typeof status.vector?.storeAvailable).toBe("boolean");
427+
expect(status.vector?.storeAvailable).toBe(available);
428+
expect(status.vector?.semanticAvailable).toBeUndefined();
429+
expect(status.vector?.available).toBeUndefined();
430+
});
431+
412432
it("caches embedding probe readiness across transient status managers", async () => {
413433
const cfg = createCfg({ storePath: path.join(workspaceDir, "index-probe-cache.sqlite") });
414434
const first = requireManager(

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export abstract class MemoryManagerSyncOps {
160160
protected abstract readonly vector: {
161161
enabled: boolean;
162162
available: boolean | null;
163+
semanticAvailable?: boolean;
163164
extensionPath?: string;
164165
loadError?: string;
165166
dims?: number;
@@ -213,6 +214,7 @@ export abstract class MemoryManagerSyncOps {
213214
protected resetVectorState(): void {
214215
this.vectorReady = null;
215216
this.vector.available = null;
217+
this.vector.semanticAvailable = undefined;
216218
this.vector.loadError = undefined;
217219
this.vector.dims = undefined;
218220
this.vectorDegradedWriteWarningShown = false;

0 commit comments

Comments
 (0)