Embedding services in Agent Memory are pluggable providers (remote OpenAI-compatible APIs, a local offline model via node-llama-cpp, or disabled entirely) configured through EmbeddingConfig; local and remote providers have different initialization, timeout, and input-length behaviors. The local embedding provider (embeddinggemma-300m) has a four-state lifecycle and outputs sanitized L2-normalized vectors, while remote providers are stateless; both must be queried before falling back to keyword-only search.
Vector search (embedding) is disabled by default — EmbeddingConfig.provider defaults to "none". Setting it to any other value (e.g. "openai", "deepseek") treats the target as an OpenAI-compatible remote provider; "zeroentropy" routes through ZeroEntropy's native /v1/models/embed protocol; "qclaw" forwards requests through a local proxy specified by proxyUrl.[1][2] src/core/store/embedding.ts defines two concrete embedding providers: "openai" (OpenAI-compatible HTTP APIs, covering OpenAI, Azure OpenAI, self-hosted endpoints, and the qclaw proxy) and "local" (fully offline, via node-llama-cpp). When no remote embedding is configured, the system automatically falls back to the local provider.[3]
The OpenAIEmbeddingConfig interface in src/core/store/embedding.ts requires baseUrl, apiKey, model, and dimensions to be explicitly provided by the caller — there are no defaults for these fields.[3] EmbeddingConfig.sendDimensions defaults to true, which includes a dimensions field in the request body for OpenAI text-embedding-3-* Matryoshka truncation. Fixed-dimension models such as BGE-M3 reject this field with HTTP 400 ('does not support matryoshka representation'); set sendDimensions: false to omit it.[2][4] When embedding.provider is "qclaw", embedding.proxyUrl is required; embedding requests are forwarded through the proxy with the original baseUrl passed as a Remote-URL header.[3][2] EmbeddingConfig.maxInputChars (default 5000) truncates input text with a warning before it is sent to the API. Single API calls time out after embedding.timeoutMs ms (default 10000 ms) and auto-retry up to 3 times.[1][2] embedding.recallTimeoutMs and embedding.captureTimeoutMs override embedding.timeoutMs per code path. Because the user is waiting during recall, a shorter timeout (e.g. 3000 ms) is recommended there; background capture can safely use a longer value (e.g. 15000 ms). Both fall back to timeoutMs when not set.[1][2] EmbeddingConfig.configError is an internal field: when set, it carries an error message about invalid remote configuration and disables embedding. The field is not exposed in the plugin schema.[1] Matryoshka representation is an embedding technique in which a model is trained so that any prefix of its output vector is itself a valid, lower-dimensional embedding; only models explicitly trained this way support the dimensions field in API requests.
The local embedding provider defaults to Google's embeddinggemma-300m model (quantized Q8_0, ~300 MB), downloaded from HuggingFace via node-llama-cpp. LocalEmbeddingService outputs 768-dimensional vectors (LOCAL_DIMENSIONS = 768).[3] Local input is capped at 512 characters (LOCAL_MAX_INPUT_CHARS = 512) — a conservative universal limit driven by embeddinggemma-300m's 256-token context window. CJK text tokenizes at 1–2 tokens per character, so 600 chars risks overflow; Latin text is safe to approximately 800 chars.[3] All vectors produced by LocalEmbeddingService are sanitized (NaN/Inf values replaced with 0) and L2-normalized via sanitizeAndNormalize() before being returned — matching the behavior of OpenClaw's own sanitizeAndNormalizeEmbedding().[3]
LocalEmbeddingService has a four-state lifecycle: "idle" (not started), "initializing" (download/load in progress), "ready" (model loaded), and "failed" (initialization failed, retryable via startWarmup()).[3] startWarmup() is idempotent: calling it when state is "initializing" or "ready" is a no-op; calling it when state is "failed" re-triggers initialization. For remote (OpenAI) providers, startWarmup() is always a no-op.[3] The EmbeddingService interface defines isReady() as always returning true for remote providers (stateless HTTP) and returning true for the local provider only after model download and load complete.[3] EmbeddingNotReadyError is thrown by embed() and embedBatch() when the local model has not finished loading. Callers are expected to catch it and fall back to keyword-only mode.[3] LocalEmbeddingService.embedBatch() processes texts sequentially in a for...of loop (not in parallel) and returns an empty array immediately when passed an empty list.[3] LocalEmbeddingService.close() calls dispose() on the embedding context (if present), nulls state, and resets to "idle". The method is idempotent and safe to call multiple times.[3]
The ImportLlamaFn type in src/core/store/embedding.ts is exported and overridable via the LocalEmbeddingService constructor, enabling injection of a mock for unit testing without loading node-llama-cpp.[3]
Sources