Files
openclaw/src/plugins/memory-embedding-providers.ts
Onur Solmaz 7ff29a9e6d Fix local embedding worker safety (#85348)
Summary:
- The PR routes local GGUF memory embeddings through a bundled worker sidecar, adds structured degradation and fallback handling, updates memory tests/build output, and keeps the local config contract unchanged.
- PR surface: Source +831, Tests +503, Docs +1, Other +2. Total +1337 across 23 files.
- Reproducibility: Do we have a high-confidence way to reproduce the issue? Source and report evidence are str ... cludes native crash logs; the exact Metal teardown abort was not reproduced in this review or the PR proof.

Automerge notes:
- PR branch already contained follow-up commit before automerge: fix(memory): keep local embedding config unchanged
- PR branch already contained follow-up commit before automerge: fix(memory): type local embedding degradation
- PR branch already contained follow-up commit before automerge: fix(memory): refresh keywords after embedding fallback
- PR branch already contained follow-up commit before automerge: fix(memory): keep worker errors internal
- PR branch already contained follow-up commit before automerge: test: satisfy memory provider lifecycle harnesses
- PR branch already contained follow-up commit before automerge: fix: harden local embedding worker fallback

Validation:
- ClawSweeper review passed for head 1d1fe41c4e.
- Required merge gates passed before the squash merge.

Prepared head SHA: 1d1fe41c4e
Review: https://github.com/openclaw/openclaw/pull/85348#issuecomment-4518516047

Co-authored-by: Onur Solmaz <onur@Onurs-MacBook-Pro.local>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: osolmaz
Co-authored-by: osolmaz <2453968+osolmaz@users.noreply.github.com>
2026-05-25 11:03:04 +00:00

167 lines
4.9 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { SecretInput } from "../config/types.secrets.js";
import type { EmbeddingInput } from "../memory-host-sdk/host/embedding-inputs.js";
export type MemoryEmbeddingBatchChunk = {
text: string;
embeddingInput?: EmbeddingInput;
};
export type MemoryEmbeddingBatchOptions = {
agentId: string;
chunks: MemoryEmbeddingBatchChunk[];
wait: boolean;
concurrency: number;
pollIntervalMs: number;
timeoutMs: number;
debug: (message: string, data?: Record<string, unknown>) => void;
};
export type MemoryEmbeddingProviderCallOptions = {
signal?: AbortSignal;
};
export type MemoryEmbeddingProviderRuntime = {
id: string;
cacheKeyData?: Record<string, unknown>;
inlineQueryTimeoutMs?: number;
inlineBatchTimeoutMs?: number;
batchEmbed?: (options: MemoryEmbeddingBatchOptions) => Promise<number[][] | null>;
};
export type MemoryEmbeddingProvider = {
id: string;
model: string;
maxInputTokens?: number;
embedQuery: (text: string, options?: MemoryEmbeddingProviderCallOptions) => Promise<number[]>;
embedBatch: (
texts: string[],
options?: MemoryEmbeddingProviderCallOptions,
) => Promise<number[][]>;
embedBatchInputs?: (
inputs: EmbeddingInput[],
options?: MemoryEmbeddingProviderCallOptions,
) => Promise<number[][]>;
close?: () => Promise<void> | void;
};
export type MemoryEmbeddingProviderCreateOptions = {
config: OpenClawConfig;
agentDir?: string;
provider?: string;
fallback?: string;
remote?: {
baseUrl?: string;
apiKey?: SecretInput;
headers?: Record<string, string>;
};
model: string;
inputType?: string;
queryInputType?: string;
documentInputType?: string;
local?: {
modelPath?: string;
modelCacheDir?: string;
contextSize?: number | "auto";
};
outputDimensionality?: number;
taskType?:
| "RETRIEVAL_QUERY"
| "RETRIEVAL_DOCUMENT"
| "SEMANTIC_SIMILARITY"
| "CLASSIFICATION"
| "CLUSTERING"
| "QUESTION_ANSWERING"
| "FACT_VERIFICATION";
};
export type MemoryEmbeddingProviderCreateResult = {
provider: MemoryEmbeddingProvider | null;
runtime?: MemoryEmbeddingProviderRuntime;
};
export type MemoryEmbeddingProviderAdapter = {
id: string;
defaultModel?: string;
transport?: "local" | "remote";
authProviderId?: string;
autoSelectPriority?: number;
allowExplicitWhenConfiguredAuto?: boolean;
supportsMultimodalEmbeddings?: (params: { model: string }) => boolean;
create: (
options: MemoryEmbeddingProviderCreateOptions,
) => Promise<MemoryEmbeddingProviderCreateResult>;
formatSetupError?: (err: unknown) => string;
shouldContinueAutoSelection?: (err: unknown) => boolean;
};
export type RegisteredMemoryEmbeddingProvider = {
adapter: MemoryEmbeddingProviderAdapter;
ownerPluginId?: string;
};
const MEMORY_EMBEDDING_PROVIDERS_KEY = Symbol.for("openclaw.memoryEmbeddingProviders");
function getMemoryEmbeddingProviders(): Map<string, RegisteredMemoryEmbeddingProvider> {
const globalStore = globalThis as Record<PropertyKey, unknown>;
const existing = globalStore[MEMORY_EMBEDDING_PROVIDERS_KEY];
if (existing instanceof Map) {
return existing as Map<string, RegisteredMemoryEmbeddingProvider>;
}
const created = new Map<string, RegisteredMemoryEmbeddingProvider>();
globalStore[MEMORY_EMBEDDING_PROVIDERS_KEY] = created;
return created;
}
export function registerMemoryEmbeddingProvider(
adapter: MemoryEmbeddingProviderAdapter,
options?: { ownerPluginId?: string },
): void {
getMemoryEmbeddingProviders().set(adapter.id, {
adapter,
ownerPluginId: options?.ownerPluginId,
});
}
export function getRegisteredMemoryEmbeddingProvider(
id: string,
): RegisteredMemoryEmbeddingProvider | undefined {
return getMemoryEmbeddingProviders().get(id);
}
export function getMemoryEmbeddingProvider(id: string): MemoryEmbeddingProviderAdapter | undefined {
return getMemoryEmbeddingProviders().get(id)?.adapter;
}
export function listRegisteredMemoryEmbeddingProviders(): RegisteredMemoryEmbeddingProvider[] {
return Array.from(getMemoryEmbeddingProviders().values());
}
export function listMemoryEmbeddingProviders(): MemoryEmbeddingProviderAdapter[] {
return listRegisteredMemoryEmbeddingProviders().map((entry) => entry.adapter);
}
export function restoreMemoryEmbeddingProviders(adapters: MemoryEmbeddingProviderAdapter[]): void {
getMemoryEmbeddingProviders().clear();
for (const adapter of adapters) {
registerMemoryEmbeddingProvider(adapter);
}
}
export function restoreRegisteredMemoryEmbeddingProviders(
entries: RegisteredMemoryEmbeddingProvider[],
): void {
getMemoryEmbeddingProviders().clear();
for (const entry of entries) {
registerMemoryEmbeddingProvider(entry.adapter, {
ownerPluginId: entry.ownerPluginId,
});
}
}
export function clearMemoryEmbeddingProviders(): void {
getMemoryEmbeddingProviders().clear();
}
export const resetMemoryEmbeddingProviders = clearMemoryEmbeddingProviders;