refactor(memory): scope embedding service acquisition

This commit is contained in:
Vincent Koc
2026-07-11 02:16:48 -07:00
committed by Vincent Koc
parent a1fb3d19ef
commit dc3c55418a
8 changed files with 116 additions and 53 deletions

View File

@@ -7,15 +7,3 @@ export type MemoryCoreAcquireLocalService = (
},
signal?: AbortSignal | null,
) => Promise<{ release: () => void } | undefined>;
let acquireLocalService: MemoryCoreAcquireLocalService | undefined;
export function configureMemoryCoreEmbeddingLocalService(
acquire: MemoryCoreAcquireLocalService | undefined,
): void {
acquireLocalService = acquire;
}
export function getMemoryCoreEmbeddingLocalService(): MemoryCoreAcquireLocalService | undefined {
return acquireLocalService;
}

View File

@@ -3,7 +3,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import type { EmbeddingProviderAdapter } from "openclaw/plugin-sdk/embedding-providers";
import type { MemoryEmbeddingProviderAdapter } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { configureMemoryCoreEmbeddingLocalService } from "./embedding-local-service.js";
import { createEmbeddingProvider, resolveEmbeddingProviderFallbackModel } from "./embeddings.js";
const mockEmbeddingRegistry = vi.hoisted(() => ({
@@ -38,7 +37,10 @@ const missingBedrockCredentialsError = new Error(
'No API key found for provider "bedrock". AWS credentials are not available.',
);
function createOptions(provider: string) {
function createOptions(
provider: string,
acquireLocalService = mockEmbeddingRegistry.acquireLocalService,
) {
return {
config: {
plugins: {
@@ -59,6 +61,7 @@ function createOptions(provider: string) {
provider,
fallback: "none",
model: "",
acquireLocalService,
};
}
@@ -102,11 +105,10 @@ function registerMemoryEmbeddingProvider(adapter: MemoryEmbeddingProviderAdapter
describe("createEmbeddingProvider", () => {
beforeEach(() => {
clearMemoryEmbeddingProviders();
configureMemoryCoreEmbeddingLocalService(mockEmbeddingRegistry.acquireLocalService);
mockEmbeddingRegistry.acquireLocalService.mockReset();
});
afterEach(() => {
configureMemoryCoreEmbeddingLocalService(undefined);
clearMemoryEmbeddingProviders();
});
@@ -194,6 +196,36 @@ describe("createEmbeddingProvider", () => {
await expect(result.provider?.embedBatch(["doc"])).resolves.toEqual([[3]]);
});
it("keeps concurrent provider creation bound to each caller's local-service hook", async () => {
const observedHooks: unknown[] = [];
registerGenericEmbeddingProvider({
id: "openai-compatible",
create: async (options) => {
observedHooks.push(
(options as typeof options & { acquireLocalService?: unknown }).acquireLocalService,
);
await Promise.resolve();
return {
provider: {
id: "generic",
model: "generic-model",
embed: async () => [1],
embedBatch: async (inputs) => inputs.map(() => [1]),
},
};
},
});
const firstAcquire = vi.fn(async () => undefined);
const secondAcquire = vi.fn(async () => undefined);
await Promise.all([
createEmbeddingProvider(createOptions("openai-compatible", firstAcquire)),
createEmbeddingProvider(createOptions("openai-compatible", secondAcquire)),
]);
expect(observedHooks).toEqual([firstAcquire, secondAcquire]);
});
it("keeps memory-specific providers authoritative during dual registration", async () => {
registerGenericEmbeddingProvider({
id: "openai-compatible",

View File

@@ -13,7 +13,7 @@ import {
type MemoryEmbeddingProviderRuntime,
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
import { formatErrorMessage } from "../dreaming-shared.js";
import { getMemoryCoreEmbeddingLocalService } from "./embedding-local-service.js";
import type { MemoryCoreAcquireLocalService } from "./embedding-local-service.js";
export type EmbeddingProvider = MemoryEmbeddingProvider;
export type EmbeddingProviderId = string;
@@ -33,6 +33,7 @@ export type EmbeddingProviderResult = {
type CreateEmbeddingProviderOptions = MemoryEmbeddingProviderCreateOptions & {
provider: EmbeddingProviderRequest;
fallback: EmbeddingProviderFallback;
acquireLocalService?: MemoryCoreAcquireLocalService;
};
const DEFAULT_MEMORY_EMBEDDING_PROVIDER = "openai";
@@ -239,7 +240,6 @@ async function createWithAdapter(
const createOptions = {
...options,
model: resolveProviderModel(adapter, options.model),
acquireLocalService: getMemoryCoreEmbeddingLocalService(),
};
const result = await adapter.create(createOptions);
return {

View File

@@ -50,6 +50,7 @@ import {
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import type { MemoryCoreAcquireLocalService } from "./embedding-local-service.js";
import {
createEmbeddingProvider,
resolveEmbeddingProviderAdapterId,
@@ -295,6 +296,7 @@ function createSessionSyncYield(total: number): () => Promise<void> {
}
export abstract class MemoryManagerSyncOps {
protected abstract readonly acquireLocalService?: MemoryCoreAcquireLocalService;
protected abstract readonly cfg: OpenClawConfig;
protected abstract readonly agentId: string;
protected abstract readonly workspaceDir: string;
@@ -2724,6 +2726,7 @@ export abstract class MemoryManagerSyncOps {
const fallbackResult = await createEmbeddingProvider({
config: this.cfg,
agentDir: resolveAgentDir(this.cfg, this.agentId),
...(this.acquireLocalService ? { acquireLocalService: this.acquireLocalService } : {}),
...fallbackRequest,
});

View File

@@ -29,6 +29,7 @@ import {
} 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 type { MemoryCoreAcquireLocalService } from "./embedding-local-service.js";
import {
createEmbeddingProvider,
resolveEmbeddingProviderAdapterTransport,
@@ -294,6 +295,7 @@ async function closeMemoryIndexManagersForScope(params: {
export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements MemorySearchManager {
private readonly cacheKey: string;
private readonly purpose: MemoryIndexManagerPurpose;
protected override readonly acquireLocalService?: MemoryCoreAcquireLocalService;
protected readonly cfg: OpenClawConfig;
protected readonly agentId: string;
protected readonly workspaceDir: string;
@@ -372,10 +374,12 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
cfg: OpenClawConfig;
agentId: string;
settings: ResolvedMemorySearchConfig;
acquireLocalService?: MemoryCoreAcquireLocalService;
}): Promise<EmbeddingProviderResult> {
return await createEmbeddingProvider({
config: params.cfg,
agentDir: resolveAgentDir(params.cfg, params.agentId),
...(params.acquireLocalService ? { acquireLocalService: params.acquireLocalService } : {}),
...resolveMemoryPrimaryProviderRequest({ settings: params.settings }),
});
}
@@ -384,6 +388,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
cfg: OpenClawConfig;
agentId: string;
purpose?: MemoryIndexManagerPurpose;
acquireLocalService?: MemoryCoreAcquireLocalService;
}): Promise<MemoryIndexManager | null> {
const { cfg, agentId } = params;
const settings = resolveMemorySearchConfig(cfg, agentId);
@@ -428,6 +433,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
settings,
providerRequirement,
purpose: params.purpose,
acquireLocalService: params.acquireLocalService,
});
// Lightweight dirty-file detection for status mode: check for unindexed
// session files on disk without triggering a full sync. This runs before
@@ -454,10 +460,12 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
providerRequirement: MemoryEmbeddingProviderRequirement;
providerResult?: EmbeddingProviderResult;
purpose?: MemoryIndexManagerPurpose;
acquireLocalService?: MemoryCoreAcquireLocalService;
}) {
super();
const effectiveSettings = resolveEffectiveMemorySearchSettings(params.settings);
this.cacheKey = params.cacheKey;
this.acquireLocalService = params.acquireLocalService;
this.purpose =
params.purpose === "status" || params.purpose === "cli" ? params.purpose : "default";
this.cfg = params.cfg;
@@ -550,6 +558,7 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
cfg: this.cfg,
agentId: this.agentId,
settings: this.settings,
acquireLocalService: this.acquireLocalService,
});
this.applyProviderResult(providerResult);
this.providerKey = this.computeProviderKey();

View File

@@ -25,6 +25,7 @@ import {
type ResolvedQmdConfig,
} from "openclaw/plugin-sdk/memory-core-host-engine-storage";
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
import type { MemoryCoreAcquireLocalService } from "./embedding-local-service.js";
const MEMORY_SEARCH_MANAGER_CACHE_KEY = Symbol.for("openclaw.memorySearchManagerCache");
type Maybe<T> = T | null;
@@ -125,6 +126,12 @@ export type MemorySearchManagerResult = {
};
export type MemorySearchManagerPurpose = "default" | "status" | "cli";
type MemorySearchManagerParams = {
cfg: OpenClawConfig;
agentId: string;
purpose?: MemorySearchManagerPurpose;
acquireLocalService?: MemoryCoreAcquireLocalService;
};
function getActiveQmdManagerOpenFailure(
scopeKey: string,
@@ -182,11 +189,9 @@ function applyManagerDebug(
};
}
export async function getMemorySearchManager(params: {
cfg: OpenClawConfig;
agentId: string;
purpose?: MemorySearchManagerPurpose;
}): Promise<MemorySearchManagerResult> {
export async function getMemorySearchManager(
params: MemorySearchManagerParams,
): Promise<MemorySearchManagerResult> {
const acquireStartedAt = Date.now();
const purpose = params.purpose ?? "default";
const finish = (
@@ -414,11 +419,7 @@ export async function getMemorySearchManager(params: {
}
async function getBuiltinMemorySearchManagerAfterQmdFailure(
params: {
cfg: OpenClawConfig;
agentId: string;
purpose?: MemorySearchManagerPurpose;
},
params: MemorySearchManagerParams,
qmdFailureReason: string | undefined,
): Promise<MemorySearchManagerResult> {
const fallback = await getBuiltinMemorySearchManager(params);
@@ -434,11 +435,9 @@ async function getBuiltinMemorySearchManagerAfterQmdFailure(
};
}
async function getBuiltinMemorySearchManager(params: {
cfg: OpenClawConfig;
agentId: string;
purpose?: MemorySearchManagerPurpose;
}): Promise<MemorySearchManagerResult> {
async function getBuiltinMemorySearchManager(
params: MemorySearchManagerParams,
): Promise<MemorySearchManagerResult> {
try {
const { MemoryIndexManager } = await loadManagerRuntime();
const manager = await MemoryIndexManager.get(params);

View File

@@ -24,7 +24,7 @@ vi.mock("./memory/index.js", () => ({
getMemorySearchManager: getMemorySearchManagerMock,
}));
import { memoryRuntime } from "./runtime-provider.js";
import { createMemoryRuntime, memoryRuntime } from "./runtime-provider.js";
describe("memoryRuntime", () => {
it("preserves manager debug metadata", async () => {
@@ -41,4 +41,26 @@ describe("memoryRuntime", () => {
agentId: "main",
});
});
it("keeps local-service acquisition scoped to each runtime instance", async () => {
const cfg = {} as OpenClawConfig;
const firstAcquire = vi.fn(async () => undefined);
const secondAcquire = vi.fn(async () => undefined);
await Promise.all([
createMemoryRuntime(firstAcquire).getMemorySearchManager({ cfg, agentId: "first" }),
createMemoryRuntime(secondAcquire).getMemorySearchManager({ cfg, agentId: "second" }),
]);
expect(getMemorySearchManagerMock).toHaveBeenCalledWith({
cfg,
agentId: "first",
acquireLocalService: firstAcquire,
});
expect(getMemorySearchManagerMock).toHaveBeenCalledWith({
cfg,
agentId: "second",
acquireLocalService: secondAcquire,
});
});
});

View File

@@ -1,28 +1,38 @@
// Memory Core provider module implements model/runtime integration.
import type { MemoryPluginRuntime } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
import { resolveMemoryBackendConfig } from "openclaw/plugin-sdk/memory-core-host-runtime-files";
import type { MemoryCoreAcquireLocalService } from "./memory/embedding-local-service.js";
import {
closeAllMemorySearchManagers,
closeMemorySearchManager,
getMemorySearchManager,
} from "./memory/index.js";
export const memoryRuntime: MemoryPluginRuntime = {
async getMemorySearchManager(params) {
const { manager, debug, error } = await getMemorySearchManager(params);
return {
manager,
debug,
error,
};
},
resolveMemoryBackendConfig(params) {
return resolveMemoryBackendConfig(params);
},
async closeAllMemorySearchManagers() {
await closeAllMemorySearchManagers();
},
async closeMemorySearchManager(params) {
await closeMemorySearchManager(params);
},
};
export function createMemoryRuntime(
acquireLocalService?: MemoryCoreAcquireLocalService,
): MemoryPluginRuntime {
return {
async getMemorySearchManager(params) {
const { manager, debug, error } = await getMemorySearchManager({
...params,
...(acquireLocalService ? { acquireLocalService } : {}),
});
return {
manager,
debug,
error,
};
},
resolveMemoryBackendConfig(params) {
return resolveMemoryBackendConfig(params);
},
async closeAllMemorySearchManagers() {
await closeAllMemorySearchManagers();
},
async closeMemorySearchManager(params) {
await closeMemorySearchManager(params);
},
};
}
export const memoryRuntime = createMemoryRuntime();