diff --git a/CHANGELOG.md b/CHANGELOG.md index bfeffb0c6b2..1db97a4fa42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai ### Fixes - 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. +- 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. - 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. - 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. - 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. diff --git a/docs/cli/memory.md b/docs/cli/memory.md index 3dba66822d9..53166609b8b 100644 --- a/docs/cli/memory.md +++ b/docs/cli/memory.md @@ -51,7 +51,7 @@ openclaw memory index --agent main --verbose `memory status`: -- `--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`. +- `--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`. - `--index`: run a reindex if the store is dirty (implies `--deep`). - `--fix`: repair stale recall locks and normalize promotion metadata. - `--json`: print JSON output. diff --git a/docs/concepts/memory-builtin.md b/docs/concepts/memory-builtin.md index 51bf8f86afa..b5a09767b21 100644 --- a/docs/concepts/memory-builtin.md +++ b/docs/concepts/memory-builtin.md @@ -127,7 +127,10 @@ when `memorySearch.local.modelPath` points to an existing local file. may miss changes in rare edge cases. **sqlite-vec not loading?** OpenClaw falls back to in-process cosine similarity -automatically. Check logs for the specific load error. +automatically. `openclaw memory status --deep` reports the local vector store +separately from the embedding provider, so `Vector store: unavailable` points +at sqlite-vec loading while `Embeddings: unavailable` points at provider/auth +or model readiness. Check logs for the specific load error. ## Configuration diff --git a/extensions/memory-core/src/cli.runtime.ts b/extensions/memory-core/src/cli.runtime.ts index 8e2d3906560..b89c0c86548 100644 --- a/extensions/memory-core/src/cli.runtime.ts +++ b/extensions/memory-core/src/cli.runtime.ts @@ -676,14 +676,30 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { let indexError: string | undefined; const syncFn = manager.sync ? manager.sync.bind(manager) : undefined; if (deep) { - await withProgress({ label: "Checking memory…", total: 2 }, async (progress) => { - progress.setLabel("Probing vector…"); - await manager.probeVectorAvailability(); - progress.tick(); - progress.setLabel("Probing embeddings…"); - embeddingProbe = await manager.probeEmbeddingAvailability(); - progress.tick(); - }); + const initialStatus = manager.status(); + const hasVectorStoreProbe = + initialStatus.backend === "builtin" && + typeof manager.probeVectorStoreAvailability === "function"; + await withProgress( + { label: "Checking memory…", total: hasVectorStoreProbe ? 3 : 2 }, + async (progress) => { + progress.setLabel(hasVectorStoreProbe ? "Probing vector store…" : "Probing vectors…"); + if (hasVectorStoreProbe) { + await manager.probeVectorStoreAvailability?.(); + } else { + await manager.probeVectorAvailability(); + } + progress.tick(); + progress.setLabel("Probing embeddings…"); + embeddingProbe = await manager.probeEmbeddingAvailability(); + progress.tick(); + if (hasVectorStoreProbe) { + progress.setLabel("Checking semantic vectors…"); + await manager.probeVectorAvailability(); + progress.tick(); + } + }, + ); if (opts.index && syncFn) { await withProgressTotals( { @@ -856,20 +872,31 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { lines.push(`${label("Fallback")} ${warn(status.fallback.from)}`); } if (status.vector) { - const vectorState = status.vector.enabled - ? status.vector.available === undefined - ? "unknown" - : status.vector.available - ? "ready" - : "unavailable" - : "disabled"; - const vectorColor = - vectorState === "ready" - ? theme.success - : vectorState === "unavailable" - ? theme.warn - : theme.muted; - lines.push(`${label("Vector")} ${colorize(rich, vectorColor, vectorState)}`); + const formatVectorState = (available: boolean | undefined) => + status.vector?.enabled + ? available === undefined + ? "unknown" + : available + ? "ready" + : "unavailable" + : "disabled"; + const formatVectorLine = (lineLabel: string, state: string) => { + const vectorColor = + state === "ready" ? theme.success : state === "unavailable" ? theme.warn : theme.muted; + lines.push(`${label(lineLabel)} ${colorize(rich, vectorColor, state)}`); + }; + if (status.backend === "builtin") { + const storeState = formatVectorState(status.vector.storeAvailable); + formatVectorLine("Vector store", storeState); + if (status.vector.semanticAvailable !== undefined) { + formatVectorLine("Semantic vectors", formatVectorState(status.vector.semanticAvailable)); + } + } else { + const vectorState = formatVectorState( + status.vector.semanticAvailable ?? status.vector.available, + ); + formatVectorLine("Vector", vectorState); + } if (status.vector.dims) { lines.push(`${label("Vector dims")} ${info(String(status.vector.dims))}`); } @@ -1117,7 +1144,8 @@ export async function runMemoryIndex(opts: MemoryCommandOptions) { } const postIndexStatus = manager.status(); const vectorEnabled = postIndexStatus.vector?.enabled ?? false; - const vectorAvailable = postIndexStatus.vector?.available; + const vectorAvailable = + postIndexStatus.vector?.storeAvailable ?? postIndexStatus.vector?.available; const vectorLoadErr = postIndexStatus.vector?.loadError; if (vectorEnabled && vectorAvailable === false) { const errDetail = vectorLoadErr ? `: ${vectorLoadErr}` : ""; diff --git a/extensions/memory-core/src/cli.test.ts b/extensions/memory-core/src/cli.test.ts index b4702f9f733..eb22c05b394 100644 --- a/extensions/memory-core/src/cli.test.ts +++ b/extensions/memory-core/src/cli.test.ts @@ -105,6 +105,7 @@ describe("memory cli", () => { function makeMemoryStatus(overrides: Record = {}) { return { + backend: "builtin", files: 0, chunks: 0, dirty: false, @@ -113,7 +114,7 @@ describe("memory cli", () => { provider: "openai", model: "text-embedding-3-small", requestedProvider: "openai", - vector: { enabled: true, available: true }, + vector: { enabled: true, storeAvailable: true, semanticAvailable: true, available: true }, ...overrides, }; } @@ -226,6 +227,8 @@ describe("memory cli", () => { fts: { enabled: true, available: true }, vector: { enabled: true, + storeAvailable: true, + semanticAvailable: true, available: true, extensionPath: "/opt/sqlite-vec.dylib", dims: 1024, @@ -238,7 +241,8 @@ describe("memory cli", () => { await runMemoryCli(["status"]); expect(probeVectorAvailability).not.toHaveBeenCalled(); - expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: ready")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Semantic vectors: ready")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector dims: 1024")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector path: /opt/sqlite-vec.dylib")); expect(log).toHaveBeenCalledWith(expect.stringContaining("FTS: ready")); @@ -274,7 +278,7 @@ describe("memory cli", () => { expect(probeVectorAvailability).not.toHaveBeenCalled(); expect(probeEmbeddingAvailability).not.toHaveBeenCalled(); expect(log).toHaveBeenCalledWith(expect.stringContaining("Provider: auto")); - expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unknown")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: unknown")); expect(close).toHaveBeenCalled(); }); @@ -350,6 +354,8 @@ describe("memory cli", () => { dirty: true, vector: { enabled: true, + storeAvailable: false, + semanticAvailable: false, available: false, loadError: "load failed", }, @@ -360,16 +366,19 @@ describe("memory cli", () => { const log = spyRuntimeLogs(defaultRuntime); await runMemoryCli(["status", "--agent", "main"]); - expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: unavailable")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: unavailable")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Semantic vectors: unavailable")); expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector error: load failed")); expect(close).toHaveBeenCalled(); }); it("prints embeddings status when deep", async () => { const close = vi.fn(async () => {}); + const probeVectorStoreAvailability = vi.fn(async () => true); const probeVectorAvailability = vi.fn(async () => true); const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); mockManager({ + probeVectorStoreAvailability, probeVectorAvailability, probeEmbeddingAvailability, status: () => makeMemoryStatus({ files: 1, chunks: 1 }), @@ -379,12 +388,89 @@ describe("memory cli", () => { const log = spyRuntimeLogs(defaultRuntime); await runMemoryCli(["status", "--deep"]); + expect(probeVectorStoreAvailability).toHaveBeenCalled(); expect(probeVectorAvailability).toHaveBeenCalled(); expect(probeEmbeddingAvailability).toHaveBeenCalled(); expect(log).toHaveBeenCalledWith(expect.stringContaining("Embeddings: ready")); expect(close).toHaveBeenCalled(); }); + it("prints vector store separately from embedding readiness when deep", async () => { + const close = vi.fn(async () => {}); + const probeVectorStoreAvailability = vi.fn(async () => true); + const probeVectorAvailability = vi.fn(async () => false); + const probeEmbeddingAvailability = vi.fn(async () => ({ + ok: false, + error: "No embedding provider available", + })); + mockManager({ + probeVectorStoreAvailability, + probeVectorAvailability, + probeEmbeddingAvailability, + status: () => + makeMemoryStatus({ + provider: "none", + requestedProvider: "auto", + vector: { + enabled: true, + storeAvailable: true, + semanticAvailable: false, + available: false, + }, + }), + close, + }); + + const log = spyRuntimeLogs(defaultRuntime); + await runMemoryCli(["status", "--deep"]); + + expect(probeVectorStoreAvailability).toHaveBeenCalled(); + expect(probeEmbeddingAvailability).toHaveBeenCalled(); + expect(probeVectorAvailability).toHaveBeenCalled(); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector store: ready")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Semantic vectors: unavailable")); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Embeddings: unavailable")); + expect(log).toHaveBeenCalledWith( + expect.stringContaining("Embeddings error: No embedding provider available"), + ); + expect(close).toHaveBeenCalled(); + }); + + it("keeps non-builtin deep status on the semantic vector probe", async () => { + const close = vi.fn(async () => {}); + const probeVectorStoreAvailability = vi.fn(async () => true); + const probeVectorAvailability = vi.fn(async () => true); + const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); + mockManager({ + probeVectorStoreAvailability, + probeVectorAvailability, + probeEmbeddingAvailability, + status: () => + makeMemoryStatus({ + backend: "qmd", + provider: "qmd", + model: "qmd", + requestedProvider: "qmd", + vector: { + enabled: true, + semanticAvailable: true, + available: true, + }, + }), + close, + }); + + const log = spyRuntimeLogs(defaultRuntime); + await runMemoryCli(["status", "--deep"]); + + expect(probeVectorStoreAvailability).not.toHaveBeenCalled(); + expect(probeVectorAvailability).toHaveBeenCalled(); + expect(probeEmbeddingAvailability).toHaveBeenCalled(); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Vector: ready")); + expect(log).not.toHaveBeenCalledWith(expect.stringContaining("Vector store:")); + expect(close).toHaveBeenCalled(); + }); + it("prints recall-store audit details during status", async () => { await withTempWorkspace(async (workspaceDir) => { await recordShortTermRecalls({ @@ -578,9 +664,11 @@ describe("memory cli", () => { it("reindexes on status --index", async () => { const close = vi.fn(async () => {}); const sync = vi.fn(async () => {}); + const probeVectorStoreAvailability = vi.fn(async () => true); const probeVectorAvailability = vi.fn(async () => true); const probeEmbeddingAvailability = vi.fn(async () => ({ ok: true })); mockManager({ + probeVectorStoreAvailability, probeVectorAvailability, probeEmbeddingAvailability, sync, @@ -592,6 +680,7 @@ describe("memory cli", () => { await runMemoryCli(["status", "--index"]); expectCliSync(sync); + expect(probeVectorStoreAvailability).toHaveBeenCalled(); expect(probeVectorAvailability).toHaveBeenCalled(); expect(probeEmbeddingAvailability).toHaveBeenCalled(); expect(getMemorySearchManager).toHaveBeenCalledWith({ diff --git a/extensions/memory-core/src/memory/index.test.ts b/extensions/memory-core/src/memory/index.test.ts index 9f280c92c9a..fc55a9078dd 100644 --- a/extensions/memory-core/src/memory/index.test.ts +++ b/extensions/memory-core/src/memory/index.test.ts @@ -406,9 +406,29 @@ describe("memory index", () => { const status = manager.status(); expect(status.vector?.enabled).toBe(true); expect(typeof status.vector?.available).toBe("boolean"); + expect(status.vector?.storeAvailable).toBe(available); + expect(status.vector?.semanticAvailable).toBe(available); expect(status.vector?.available).toBe(available); }); + it("probes sqlite vector store availability without initializing embeddings", async () => { + forceNoProvider = true; + const cfg = createCfg({ + storePath: path.join(workspaceDir, "index-vector-store-only.sqlite"), + vectorEnabled: true, + }); + const manager = await getPersistentManager(cfg); + + const available = await manager.probeVectorStoreAvailability?.(); + const status = manager.status(); + + expect(providerCalls).toEqual([]); + expect(typeof status.vector?.storeAvailable).toBe("boolean"); + expect(status.vector?.storeAvailable).toBe(available); + expect(status.vector?.semanticAvailable).toBeUndefined(); + expect(status.vector?.available).toBeUndefined(); + }); + it("caches embedding probe readiness across transient status managers", async () => { const cfg = createCfg({ storePath: path.join(workspaceDir, "index-probe-cache.sqlite") }); const first = requireManager( diff --git a/extensions/memory-core/src/memory/manager-sync-ops.ts b/extensions/memory-core/src/memory/manager-sync-ops.ts index 68988fe5b23..e6b2ffd6d29 100644 --- a/extensions/memory-core/src/memory/manager-sync-ops.ts +++ b/extensions/memory-core/src/memory/manager-sync-ops.ts @@ -160,6 +160,7 @@ export abstract class MemoryManagerSyncOps { protected abstract readonly vector: { enabled: boolean; available: boolean | null; + semanticAvailable?: boolean; extensionPath?: string; loadError?: string; dims?: number; @@ -213,6 +214,7 @@ export abstract class MemoryManagerSyncOps { protected resetVectorState(): void { this.vectorReady = null; this.vector.available = null; + this.vector.semanticAvailable = undefined; this.vector.loadError = undefined; this.vector.dims = undefined; this.vectorDegradedWriteWarningShown = false; diff --git a/extensions/memory-core/src/memory/manager.ts b/extensions/memory-core/src/memory/manager.ts index ef554becdda..331090beff9 100644 --- a/extensions/memory-core/src/memory/manager.ts +++ b/extensions/memory-core/src/memory/manager.ts @@ -120,6 +120,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem protected readonly vector: { enabled: boolean; available: boolean | null; + semanticAvailable?: boolean; extensionPath?: string; loadError?: string; dims?: number; @@ -806,7 +807,9 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem : undefined, vector: { enabled: this.vector.enabled, - available: this.vector.available ?? undefined, + storeAvailable: this.vector.available ?? undefined, + semanticAvailable: this.vector.semanticAvailable, + available: this.vector.semanticAvailable, extensionPath: this.vector.extensionPath, loadError: this.vector.loadError, dims: this.vector.dims, @@ -837,14 +840,26 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem async probeVectorAvailability(): Promise { if (!this.vector.enabled) { + this.vector.semanticAvailable = false; return false; } await this.ensureProviderInitialized(); // FTS-only mode: vector search not available if (!this.provider) { + this.vector.semanticAvailable = false; return false; } - return this.ensureVectorReady(); + const ready = await this.probeVectorStoreAvailability(); + this.vector.semanticAvailable = ready; + return ready; + } + + async probeVectorStoreAvailability(): Promise { + if (!this.vector.enabled) { + this.vector.available = false; + return false; + } + return await this.ensureVectorReady(); } private cacheProbeResult(result: MemoryEmbeddingProbeResult): MemoryEmbeddingProbeResult { diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index 612c702c38f..2d1a1297a58 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -4772,6 +4772,7 @@ describe("QmdMemoryManager", () => { expect(manager.status().vector).toEqual({ enabled: true, available: false, + semanticAvailable: false, loadError: "QMD index has 0 vectors; semantic search is unavailable until embeddings finish", }); await manager.close(); @@ -4805,6 +4806,7 @@ describe("QmdMemoryManager", () => { expect(manager.status().vector).toEqual({ enabled: true, available: true, + semanticAvailable: true, loadError: undefined, }); await manager.close(); @@ -4863,6 +4865,7 @@ describe("QmdMemoryManager", () => { expect(manager.status().vector).toEqual({ enabled: true, available: false, + semanticAvailable: false, loadError: "Could not determine QMD vector status from `qmd status`", }); await manager.close(); @@ -4889,6 +4892,7 @@ describe("QmdMemoryManager", () => { expect(manager.status().vector).toEqual({ enabled: false, available: false, + semanticAvailable: false, loadError: undefined, }); await manager.close(); diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index ac5bc0f646e..864e6444e63 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -1357,6 +1357,7 @@ export class QmdMemoryManager implements MemorySearchManager { vector: { enabled: qmdUsesVectors(this.qmd.searchMode), available: this.vectorAvailable ?? undefined, + semanticAvailable: this.vectorAvailable ?? undefined, loadError: this.vectorStatusDetail ?? undefined, }, batch: { diff --git a/extensions/memory-core/src/memory/search-manager.ts b/extensions/memory-core/src/memory/search-manager.ts index 42ff98ada10..b19544ba983 100644 --- a/extensions/memory-core/src/memory/search-manager.ts +++ b/extensions/memory-core/src/memory/search-manager.ts @@ -325,7 +325,14 @@ async function getBuiltinMemorySearchManager(params: { } class BorrowedMemoryManager implements MemorySearchManager { - constructor(private readonly inner: MemorySearchManager) {} + readonly probeVectorStoreAvailability?: () => Promise; + + constructor(private readonly inner: MemorySearchManager) { + if (inner.probeVectorStoreAvailability) { + const probeVectorStoreAvailability = inner.probeVectorStoreAvailability.bind(inner); + this.probeVectorStoreAvailability = async () => await probeVectorStoreAvailability(); + } + } async search( query: string, @@ -517,6 +524,19 @@ class FallbackMemoryManager implements MemorySearchManager { return this.fallback?.getCachedEmbeddingAvailability?.() ?? null; } + async probeVectorStoreAvailability() { + this.ensureOpen(); + if (!this.primaryFailed) { + return await (this.deps.primary.probeVectorStoreAvailability?.() ?? + this.deps.primary.probeVectorAvailability()); + } + const fallback = await this.ensureFallback(); + return ( + (await (fallback?.probeVectorStoreAvailability?.() ?? fallback?.probeVectorAvailability())) ?? + false + ); + } + async probeVectorAvailability() { this.ensureOpen(); if (!this.primaryFailed) { diff --git a/packages/memory-host-sdk/src/host/types.ts b/packages/memory-host-sdk/src/host/types.ts index 7c99da2d32f..9c7de1ab9ce 100644 --- a/packages/memory-host-sdk/src/host/types.ts +++ b/packages/memory-host-sdk/src/host/types.ts @@ -61,6 +61,8 @@ export type MemoryProviderStatus = { fallback?: { from: string; reason?: string }; vector?: { enabled: boolean; + storeAvailable?: boolean; + semanticAvailable?: boolean; available?: boolean; extensionPath?: string; loadError?: string; @@ -102,6 +104,7 @@ export interface MemorySearchManager { }): Promise; getCachedEmbeddingAvailability?(): MemoryEmbeddingProbeResult | null; probeEmbeddingAvailability(): Promise; + probeVectorStoreAvailability?(): Promise; probeVectorAvailability(): Promise; close?(): Promise; } diff --git a/src/commands/status.command-sections.ts b/src/commands/status.command-sections.ts index fd8b42705eb..8854206a409 100644 --- a/src/commands/status.command-sections.ts +++ b/src/commands/status.command-sections.ts @@ -168,8 +168,13 @@ export function buildStatusMemoryValue( const colorByTone = (tone: Tone, text: string) => tone === "ok" ? params.ok(text) : tone === "warn" ? params.warn(text) : params.muted(text); if (params.memory.vector) { - const state = params.resolveMemoryVectorState(params.memory.vector); - const label = state.state === "disabled" ? "vector off" : `vector ${state.state}`; + const vector = + params.memory.backend === "builtin" && params.memory.vector.storeAvailable !== undefined + ? { ...params.memory.vector, available: params.memory.vector.storeAvailable } + : params.memory.vector; + const state = params.resolveMemoryVectorState(vector); + const prefix = params.memory.backend === "builtin" ? "vector store" : "vector"; + const label = state.state === "disabled" ? `${prefix} off` : `${prefix} ${state.state}`; parts.push(colorByTone(state.tone, label)); } if (params.memory.fts) { diff --git a/src/commands/status.scan.deps.runtime.ts b/src/commands/status.scan.deps.runtime.ts index 8a5dce72685..558aa7c7457 100644 --- a/src/commands/status.scan.deps.runtime.ts +++ b/src/commands/status.scan.deps.runtime.ts @@ -6,6 +6,7 @@ import { getActiveMemorySearchManager } from "../plugins/memory-runtime.js"; export { getTailnetHostname }; type StatusMemoryManager = { + probeVectorStoreAvailability?(): Promise; probeVectorAvailability(): Promise; status(): MemoryProviderStatus; close?(): Promise; @@ -20,8 +21,12 @@ export async function getMemorySearchManager(params: { if (!manager) { return { manager: null }; } + const probeVectorStoreAvailability = manager.probeVectorStoreAvailability + ? async () => await manager.probeVectorStoreAvailability!() + : undefined; return { manager: { + probeVectorStoreAvailability, async probeVectorAvailability() { return await manager.probeVectorAvailability(); }, diff --git a/src/commands/status.scan.shared.test.ts b/src/commands/status.scan.shared.test.ts index 3707af8bae8..d84c79f82bd 100644 --- a/src/commands/status.scan.shared.test.ts +++ b/src/commands/status.scan.shared.test.ts @@ -406,6 +406,7 @@ describe("resolveGatewayProbeSnapshot", () => { describe("resolveSharedMemoryStatusSnapshot", () => { it("asks custom memory-slot runtimes for status without requiring built-in memorySearch", async () => { const manager = { + probeVectorStoreAvailability: vi.fn(async () => true), probeVectorAvailability: vi.fn(async () => true), status: vi.fn(() => ({ backend: "builtin" as const, @@ -450,7 +451,8 @@ describe("resolveSharedMemoryStatusSnapshot", () => { agentId: "main", purpose: "status", }); - expect(manager.probeVectorAvailability).toHaveBeenCalled(); + expect(manager.probeVectorStoreAvailability).toHaveBeenCalled(); + expect(manager.probeVectorAvailability).not.toHaveBeenCalled(); expect(manager.status).toHaveBeenCalled(); expect(manager.close).toHaveBeenCalled(); expect(result).toEqual({ @@ -464,6 +466,42 @@ describe("resolveSharedMemoryStatusSnapshot", () => { }); }); + it("uses semantic vector probes for non-builtin memory-slot runtimes", async () => { + const manager = { + probeVectorStoreAvailability: vi.fn(async () => true), + probeVectorAvailability: vi.fn(async () => true), + status: vi.fn(() => ({ + backend: "qmd" as const, + provider: "qmd", + files: 5, + chunks: 5, + vector: { enabled: true, available: true, semanticAvailable: true }, + })), + close: vi.fn(async () => {}), + }; + const getMemorySearchManager = vi.fn(async () => ({ manager })); + + const result = await resolveSharedMemoryStatusSnapshot({ + cfg: { plugins: { slots: { memory: "qmd" } } }, + agentStatus: { defaultId: "main" }, + memoryPlugin: { enabled: true, slot: "qmd" }, + resolveMemoryConfig: vi.fn(() => null), + getMemorySearchManager, + requireDefaultStore: vi.fn(), + }); + + expect(manager.probeVectorStoreAvailability).not.toHaveBeenCalled(); + expect(manager.probeVectorAvailability).toHaveBeenCalled(); + expect(result).toEqual({ + agentId: "main", + backend: "qmd", + provider: "qmd", + files: 5, + chunks: 5, + vector: { enabled: true, available: true, semanticAvailable: true }, + }); + }); + it("keeps default memory-core on the cold-start store shortcut", async () => { const resolveMemoryConfig = vi.fn(() => null); const getMemorySearchManager = vi.fn(async () => ({ manager: null })); diff --git a/src/commands/status.scan.shared.ts b/src/commands/status.scan.shared.ts index c179dbe2d30..fa5b626c704 100644 --- a/src/commands/status.scan.shared.ts +++ b/src/commands/status.scan.shared.ts @@ -63,6 +63,7 @@ export type GatewayProbeSnapshot = { }; type StatusMemorySearchManager = { + probeVectorStoreAvailability?(): Promise; probeVectorAvailability(): Promise; status(): MemoryProviderStatus; close?(): Promise; @@ -336,7 +337,12 @@ async function resolveMemoryManagerStatusSnapshot( } try { try { - await manager.probeVectorAvailability(); + const currentStatus = manager.status(); + if (currentStatus.backend === "builtin" && manager.probeVectorStoreAvailability) { + await manager.probeVectorStoreAvailability(); + } else { + await manager.probeVectorAvailability(); + } } catch {} const status = manager.status(); return { agentId, ...status };