Skip to content

memory search returns 0 results in FTS-only mode (no embedding provider) #51782

Description

@vahawa

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

  1. Configure OpenClaw with no embedding provider (default memory settings, no API key)
  2. Add memory files (e.g. via the agent)
  3. Run openclaw memory index --agent main
  4. Run openclaw memory search --agent main --json <term>{"results": []}
  5. Verify: openclaw memory status --agent main --deep --json shows files: 0, chunks: 0 despite scan.totalFiles > 0 (Bug 1)
  6. After fixing Bug 1 and reindexing, search still returns {"results": []} (Bug 2)
  7. Direct SQLite query against the same DB with the same FTS MATCH confirms results exist

Suggested Fix

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions