Bug Description
When no embedding provider is configured (searchMode: fts-only), memory search always returns empty results despite memory files existing and memory status --deep reporting scan.totalFiles > 0.
Two independent bugs cause this:
Bug 1: Indexing is completely skipped in FTS-only mode
Three early-return guards prevent all indexing — including FTS — when this.provider is null:
src/memory/manager-sync-ops.ts syncMemoryFiles() (~L700): if (!this.provider) return;
src/memory/manager-sync-ops.ts syncSessionFiles() (~L792): if (!this.provider) return;
src/memory/manager-embedding-ops.ts indexFile() (~L803): if (!this.provider) return;
These guards should only skip embedding generation, not the entire sync/index pipeline. In FTS-only mode, chunks still need to be inserted into chunks and chunks_fts — just without embeddings.
Additionally, syncMemoryFiles references this.provider.model without null-check in the stale FTS cleanup path, which throws a TypeError if the guards are removed without also making this null-safe.
Bug 2: minScore filter is too strict for FTS-only results
In src/memory/manager.ts search(), the FTS-only code path (inside if (!this.provider)) applies the same minScore threshold (default 0.35) as the hybrid path. However, bm25RankToScore() returns very small values (~0.000001) because raw BM25 ranks from SQLite FTS5 are close to zero for typical short-document queries.
In hybrid mode this works because scores are normalized during mergeHybridResults(). In FTS-only mode there is no such normalization, so all results are silently filtered out by the minScore check — even when FTS found relevant matches.
Even if Bug 1 is fixed, Bug 2 causes the search to still return empty results.
Reproduction
- Configure OpenClaw with no embedding provider (default memory settings, no API key)
- Add memory files (e.g. via the agent)
- Run
openclaw memory index --agent main
- Run
openclaw memory search --agent main --json <term> → {"results": []}
- Verify:
openclaw memory status --agent main --deep --json shows files: 0, chunks: 0 despite scan.totalFiles > 0 (Bug 1)
- After fixing Bug 1 and reindexing, search still returns
{"results": []} (Bug 2)
- Direct SQLite query against the same DB with the same FTS MATCH confirms results exist
Suggested Fix
- Bug 1: Remove the three
if (!this.provider) return; guards and let indexFile run without embeddings. Use this.provider?.model ?? "fts-only" (or similar) wherever this.provider.model is referenced, and adjust the stale-FTS-cleanup SQL to omit the AND model = ? clause when provider is null.
- Bug 2: In the FTS-only branch of
search(), either skip the minScore filter entirely, or normalize BM25 scores independently (e.g. min-max normalization over the result set).
Environment
- OpenClaw version: v2026.3.14 (built from
main at cdf49f0b00)
- Node.js: v24.14.0
- SQLite FTS5 available and enabled (
hybrid.enabled: true default)
- No embedding provider configured
Note
We found a stash commit (ccbd315ab9, "fix(memory): index FTS in provider-missing mode", dated 2026-02-17) in the repo that addresses Bug 1 but not Bug 2. It appears this fix was started but never merged.
Bug Description
When no embedding provider is configured (
searchMode: fts-only),memory searchalways returns empty results despite memory files existing andmemory status --deepreportingscan.totalFiles > 0.Two independent bugs cause this:
Bug 1: Indexing is completely skipped in FTS-only mode
Three early-return guards prevent all indexing — including FTS — when
this.provideris null:src/memory/manager-sync-ops.tssyncMemoryFiles()(~L700):if (!this.provider) return;src/memory/manager-sync-ops.tssyncSessionFiles()(~L792):if (!this.provider) return;src/memory/manager-embedding-ops.tsindexFile()(~L803):if (!this.provider) return;These guards should only skip embedding generation, not the entire sync/index pipeline. In FTS-only mode, chunks still need to be inserted into
chunksandchunks_fts— just without embeddings.Additionally,
syncMemoryFilesreferencesthis.provider.modelwithout null-check in the stale FTS cleanup path, which throws a TypeError if the guards are removed without also making this null-safe.Bug 2:
minScorefilter is too strict for FTS-only resultsIn
src/memory/manager.tssearch(), the FTS-only code path (insideif (!this.provider)) applies the sameminScorethreshold (default0.35) as the hybrid path. However,bm25RankToScore()returns very small values (~0.000001) because raw BM25 ranks from SQLite FTS5 are close to zero for typical short-document queries.In hybrid mode this works because scores are normalized during
mergeHybridResults(). In FTS-only mode there is no such normalization, so all results are silently filtered out by theminScorecheck — even when FTS found relevant matches.Even if Bug 1 is fixed, Bug 2 causes the search to still return empty results.
Reproduction
openclaw memory index --agent mainopenclaw memory search --agent main --json <term>→{"results": []}openclaw memory status --agent main --deep --jsonshowsfiles: 0, chunks: 0despitescan.totalFiles > 0(Bug 1){"results": []}(Bug 2)Suggested Fix
if (!this.provider) return;guards and letindexFilerun without embeddings. Usethis.provider?.model ?? "fts-only"(or similar) whereverthis.provider.modelis referenced, and adjust the stale-FTS-cleanup SQL to omit theAND model = ?clause when provider is null.search(), either skip theminScorefilter entirely, or normalize BM25 scores independently (e.g. min-max normalization over the result set).Environment
mainatcdf49f0b00)hybrid.enabled: truedefault)Note
We found a stash commit (
ccbd315ab9, "fix(memory): index FTS in provider-missing mode", dated 2026-02-17) in the repo that addresses Bug 1 but not Bug 2. It appears this fix was started but never merged.