mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(memory): keep provider requirement internal
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -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",
|
||||
}));
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user