fix(memory): keep provider requirement internal

This commit is contained in:
Onur Solmaz
2026-06-04 21:39:27 +08:00
parent a3cf280690
commit dafc13f2d6
8 changed files with 112 additions and 41 deletions

View File

@@ -158,6 +158,17 @@ export function resolveEmbeddingProviderAdapterId(
}
}
export function resolveEmbeddingProviderAdapterTransport(
providerId: string,
config?: MemoryEmbeddingProviderCreateOptions["config"],
): MemoryEmbeddingProviderAdapter["transport"] {
try {
return getAdapter(providerId, config).transport;
} catch {
return undefined;
}
}
async function createWithAdapter(
adapter: MemoryEmbeddingProviderAdapter,
options: CreateEmbeddingProviderOptions,

View File

@@ -68,6 +68,8 @@ vi.mock("./embeddings.js", () => {
};
},
) => config?.models?.providers?.[providerId]?.api ?? providerId,
resolveEmbeddingProviderAdapterTransport: (providerId: string) =>
providerId === "local" ? "local" : "remote",
createEmbeddingProvider: async (options: {
provider?: string;
model?: string;

View File

@@ -40,6 +40,8 @@ vi.mock("openclaw/plugin-sdk/memory-core-host-engine-qmd", () => {
vi.mock("./embeddings.js", () => ({
resolveEmbeddingProviderAdapterId: (providerId: string) => providerId,
resolveEmbeddingProviderAdapterTransport: (providerId: string) =>
providerId === "local" ? "local" : "remote",
createEmbeddingProvider: vi.fn(),
}));

View File

@@ -21,6 +21,8 @@ const createEmbeddingProviderMock = vi.hoisted(() =>
vi.mock("./embeddings.js", () => ({
createEmbeddingProvider: createEmbeddingProviderMock,
resolveEmbeddingProviderAdapterId: (providerId: string) => providerId,
resolveEmbeddingProviderAdapterTransport: (providerId: string) =>
providerId === "local" ? "local" : "remote",
resolveEmbeddingProviderFallbackModel: () => "fts-only",
}));

View File

@@ -22,9 +22,11 @@ import {
type MemorySource,
type MemorySyncProgressUpdate,
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { uniqueValues } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
createEmbeddingProvider,
resolveEmbeddingProviderAdapterTransport,
type EmbeddingProvider,
type EmbeddingProviderId,
type EmbeddingProviderRequest,
@@ -71,6 +73,11 @@ const MEMORY_INDEX_MANAGER_CACHE_KEY = Symbol.for("openclaw.memoryIndexManagerCa
export const EMBEDDING_PROBE_CACHE_TTL_MS = 30_000;
const log = createSubsystemLogger("memory");
type MemoryIndexManagerPurpose = "default" | "status" | "cli";
type MemoryEmbeddingProviderRequirement = {
mode: "fts-only" | "optional" | "required";
provider: string;
configuredProvider?: string;
};
const { cache: INDEX_CACHE, pending: INDEX_CACHE_PENDING } =
resolveSingletonManagedCache<MemoryIndexManager>(MEMORY_INDEX_MANAGER_CACHE_KEY);
@@ -103,7 +110,18 @@ export async function closeMemoryIndexManagersForAgent(params: {
return;
}
const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.agentId);
const key = `${params.agentId}:${workspaceDir}:${JSON.stringify(settings)}:default`;
const providerRequirement = resolveMemoryEmbeddingProviderRequirement({
cfg: params.cfg,
agentId: params.agentId,
settings,
});
const key = resolveMemoryIndexManagerCacheKey({
agentId: params.agentId,
workspaceDir,
settings,
providerRequirement,
purpose: "default",
});
const pending = INDEX_CACHE_PENDING.get(key);
if (pending) {
await Promise.allSettled([pending]);
@@ -138,6 +156,56 @@ function resolveEffectiveMemorySearchSettings(
};
}
function resolveConfiguredMemoryEmbeddingProvider(params: {
cfg: OpenClawConfig;
agentId: string;
}): string | undefined {
const normalizedAgentId = normalizeAgentId(params.agentId);
const agentEntry = params.cfg.agents?.list?.find(
(entry) => entry && normalizeAgentId(entry.id) === normalizedAgentId,
);
return agentEntry?.memorySearch?.provider ?? params.cfg.agents?.defaults?.memorySearch?.provider;
}
function resolveMemoryEmbeddingProviderRequirement(params: {
cfg: OpenClawConfig;
agentId: string;
settings: ResolvedMemorySearchConfig;
}): MemoryEmbeddingProviderRequirement {
const configuredProvider = resolveConfiguredMemoryEmbeddingProvider(params)?.trim();
if (params.settings.provider === "none" || configuredProvider === "none") {
return { mode: "fts-only", provider: params.settings.provider };
}
const adapterTransport = resolveEmbeddingProviderAdapterTransport(
params.settings.provider,
params.cfg,
);
if (!configuredProvider || configuredProvider === "auto" || adapterTransport === "local") {
return { mode: "optional", provider: params.settings.provider };
}
return {
mode: "required",
provider: params.settings.provider,
configuredProvider,
};
}
function resolveMemoryIndexManagerCacheKey(params: {
agentId: string;
workspaceDir: string;
settings: ResolvedMemorySearchConfig;
providerRequirement: MemoryEmbeddingProviderRequirement;
purpose: MemoryIndexManagerPurpose;
}): string {
return [
params.agentId,
params.workspaceDir,
JSON.stringify(params.settings),
JSON.stringify(params.providerRequirement),
params.purpose,
].join(":");
}
export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements MemorySearchManager {
private readonly cacheKey: string;
private readonly purpose: MemoryIndexManagerPurpose;
@@ -145,6 +213,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
protected readonly agentId: string;
protected readonly workspaceDir: string;
protected readonly settings: ResolvedMemorySearchConfig;
private readonly providerRequirement: MemoryEmbeddingProviderRequirement;
protected override provider: EmbeddingProvider | null;
private readonly requestedProvider: EmbeddingProviderRequest;
private providerInitPromise: Promise<void> | null = null;
@@ -237,7 +306,18 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
const purpose =
params.purpose === "status" || params.purpose === "cli" ? params.purpose : "default";
const key = `${agentId}:${workspaceDir}:${JSON.stringify(settings)}:${purpose}`;
const providerRequirement = resolveMemoryEmbeddingProviderRequirement({
cfg,
agentId,
settings,
});
const key = resolveMemoryIndexManagerCacheKey({
agentId,
workspaceDir,
settings,
providerRequirement,
purpose,
});
const transient = purpose === "status" || purpose === "cli";
return await getOrCreateManagedCacheEntry({
cache: INDEX_CACHE,
@@ -251,6 +331,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
agentId,
workspaceDir,
settings,
providerRequirement,
purpose: params.purpose,
}),
});
@@ -262,6 +343,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
agentId: string;
workspaceDir: string;
settings: ResolvedMemorySearchConfig;
providerRequirement: MemoryEmbeddingProviderRequirement;
providerResult?: EmbeddingProviderResult;
purpose?: MemoryIndexManagerPurpose;
}) {
@@ -274,6 +356,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
this.agentId = params.agentId;
this.workspaceDir = params.workspaceDir;
this.settings = effectiveSettings;
this.providerRequirement = params.providerRequirement;
this.provider = null;
this.requestedProvider = effectiveSettings.provider;
this.providerLifecycle = createPendingMemoryProviderLifecycle(this.requestedProvider);
@@ -412,7 +495,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
protected isRequiredProviderUnavailable(): boolean {
return (
this.settings.providerRequirement.mode === "required" &&
this.providerRequirement.mode === "required" &&
!this.provider &&
this.providerLifecycle.mode === "fts-only" &&
this.providerLifecycle.attemptedProviderId === this.settings.provider
@@ -495,7 +578,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
},
): Promise<MemorySearchResult[]> {
opts?.onDebug?.({ backend: "builtin" });
if (this.settings.providerRequirement.mode === "required") {
if (this.providerRequirement.mode === "required") {
await this.ensureProviderInitialized();
this.assertRequiredProviderAvailable("search");
}

View File

@@ -140,6 +140,8 @@ vi.mock("./sqlite-vec.js", () => ({
vi.mock("./embeddings.js", () => ({
resolveEmbeddingProviderAdapterId: (providerId: string) => providerId,
resolveEmbeddingProviderAdapterTransport: (providerId: string) =>
providerId === "local" ? "local" : "remote",
createEmbeddingProvider: async () => ({
requestedProvider: "openai",
provider: {

View File

@@ -204,7 +204,6 @@ describe("memory search config", () => {
expect(resolved?.provider).toBe("openai");
expect(resolved?.model).toBe("text-embedding-3-small");
expect(resolved?.fallback).toBe("none");
expect(resolved?.providerRequirement).toEqual({ mode: "optional", provider: "openai" });
});
it("normalizes legacy auto provider config to openai", () => {
@@ -212,29 +211,24 @@ describe("memory search config", () => {
expect(resolved?.provider).toBe("openai");
expect(resolved?.model).toBe("text-embedding-3-small");
expect(resolved?.providerRequirement).toEqual({ mode: "optional", provider: "openai" });
});
it("marks explicit concrete providers as required", () => {
it("resolves explicit concrete providers", () => {
const resolved = resolveMemorySearchConfig(configWithDefaultProvider("openai"), "main");
expect(resolved?.providerRequirement).toEqual({
mode: "required",
provider: "openai",
configuredProvider: "openai",
});
expect(resolved?.provider).toBe("openai");
});
it("keeps explicit local providers optional so local degradation can fall back", () => {
it("resolves explicit local providers", () => {
const resolved = resolveMemorySearchConfig(configWithDefaultProvider("local"), "main");
expect(resolved?.providerRequirement).toEqual({ mode: "optional", provider: "local" });
expect(resolved?.provider).toBe("local");
});
it("marks explicit provider-none as fts-only", () => {
it("resolves explicit provider-none", () => {
const resolved = resolveMemorySearchConfig(configWithDefaultProvider("none"), "main");
expect(resolved?.providerRequirement).toEqual({ mode: "fts-only", provider: "none" });
expect(resolved?.provider).toBe("none");
});
it("resolves custom provider ids through their configured api owner", () => {

View File

@@ -34,11 +34,6 @@ export type ResolvedMemorySearchConfig = {
extraPaths: string[];
multimodal: MemoryMultimodalSettings;
provider: string;
providerRequirement: {
mode: "fts-only" | "optional" | "required";
provider: string;
configuredProvider?: string;
};
remote?: {
baseUrl?: string;
apiKey?: SecretInput;
@@ -213,21 +208,6 @@ function getConfiguredMemoryEmbeddingProvider(
return getMemoryEmbeddingProvider(normalizedOwner);
}
function resolveMemoryEmbeddingProviderRequirement(
rawProvider: string | undefined,
provider: string,
adapterTransport?: "local" | "remote",
): ResolvedMemorySearchConfig["providerRequirement"] {
const configuredProvider = rawProvider?.trim();
if (configuredProvider === "none") {
return { mode: "fts-only", provider };
}
if (!configuredProvider || configuredProvider === "auto" || adapterTransport === "local") {
return { mode: "optional", provider };
}
return { mode: "required", provider, configuredProvider };
}
function mergeConfig(
cfg: OpenClawConfig,
defaults: MemorySearchConfig | undefined,
@@ -405,11 +385,6 @@ function mergeConfig(
extraPaths,
multimodal,
provider,
providerRequirement: resolveMemoryEmbeddingProviderRequirement(
rawProvider,
provider,
primaryAdapter?.transport,
),
remote,
experimental: {
sessionMemory,