mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(memory): split vector store readiness
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}` : "";
|
||||
|
||||
@@ -105,6 +105,7 @@ describe("memory cli", () => {
|
||||
|
||||
function makeMemoryStatus(overrides: Record<string, unknown> = {}) {
|
||||
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({
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
if (!this.vector.enabled) {
|
||||
this.vector.available = false;
|
||||
return false;
|
||||
}
|
||||
return await this.ensureVectorReady();
|
||||
}
|
||||
|
||||
private cacheProbeResult(result: MemoryEmbeddingProbeResult): MemoryEmbeddingProbeResult {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -325,7 +325,14 @@ async function getBuiltinMemorySearchManager(params: {
|
||||
}
|
||||
|
||||
class BorrowedMemoryManager implements MemorySearchManager {
|
||||
constructor(private readonly inner: MemorySearchManager) {}
|
||||
readonly probeVectorStoreAvailability?: () => Promise<boolean>;
|
||||
|
||||
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) {
|
||||
|
||||
@@ -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<void>;
|
||||
getCachedEmbeddingAvailability?(): MemoryEmbeddingProbeResult | null;
|
||||
probeEmbeddingAvailability(): Promise<MemoryEmbeddingProbeResult>;
|
||||
probeVectorStoreAvailability?(): Promise<boolean>;
|
||||
probeVectorAvailability(): Promise<boolean>;
|
||||
close?(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getActiveMemorySearchManager } from "../plugins/memory-runtime.js";
|
||||
export { getTailnetHostname };
|
||||
|
||||
type StatusMemoryManager = {
|
||||
probeVectorStoreAvailability?(): Promise<boolean>;
|
||||
probeVectorAvailability(): Promise<boolean>;
|
||||
status(): MemoryProviderStatus;
|
||||
close?(): Promise<void>;
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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 }));
|
||||
|
||||
@@ -63,6 +63,7 @@ export type GatewayProbeSnapshot = {
|
||||
};
|
||||
|
||||
type StatusMemorySearchManager = {
|
||||
probeVectorStoreAvailability?(): Promise<boolean>;
|
||||
probeVectorAvailability(): Promise<boolean>;
|
||||
status(): MemoryProviderStatus;
|
||||
close?(): Promise<void>;
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user