Skip to content

fix: enable FTS fallback when no embedding provider available (#17725)#17822

Closed
irchelper wants to merge 2 commits into
openclaw:mainfrom
irchelper:fix/17725-fts-fallback
Closed

fix: enable FTS fallback when no embedding provider available (#17725)#17822
irchelper wants to merge 2 commits into
openclaw:mainfrom
irchelper:fix/17725-fts-fallback

Conversation

@irchelper

@irchelper irchelper commented Feb 16, 2026

Copy link
Copy Markdown

Summary

Fixes #17725 - memory_search always returns disabled when using OAuth

When no embedding provider is available (e.g., OAuth mode without API keys), memory_search now falls back to FTS-only mode instead of returning disabled: true.

Changes

1. src/memory/embeddings.ts

  • Modified createEmbeddingProvider() to return { provider: null, providerUnavailableReason: ... } instead of throwing when all providers fail due to missing API keys
  • Non-auth errors (e.g., network issues) still throw as before

2. src/memory/manager.ts

  • Made provider field nullable
  • Added FTS-only search path when provider is null
  • Updated status() to report searchMode: 'fts-only' or 'hybrid'
  • Updated probeEmbeddingAvailability() and probeVectorAvailability() to handle null provider

3. src/memory/manager-search.ts

  • Made providerModel parameter optional in searchKeyword()
  • When undefined, searches across all models (FTS-only mode)

4. src/memory/manager-embedding-ops.ts

  • Updated computeProviderKey() to handle null provider

5. src/agents/tools/memory-tool.ts

  • Added mode field to search results ('hybrid' or 'fts-only')

Testing

  • Added 3 new unit tests for FTS-only fallback scenarios
  • All 121 memory tests pass
  • pnpm check passes

Verification

With this change:

  1. memory_search no longer returns disabled: true when no embedding provider
  2. ✅ FTS-only mode returns keyword-matched results
  3. ✅ Hybrid mode behavior unchanged when embeddings are available
  4. ✅ Results include mode: 'fts-only' or mode: 'hybrid'

Greptile Summary

Enables FTS-only fallback when no embedding provider is available, fixing the OAuth mode issue where memory_search returned disabled: true.

Major changes:

  • Modified createEmbeddingProvider() to return null provider with reason instead of throwing when API keys are missing
  • Updated MemoryIndexManager to handle null provider and use FTS-only search path
  • Added searchMode field to indicate 'fts-only' or 'hybrid' mode
  • Made providerModel parameter optional in searchKeyword() to search all models in FTS-only mode

Critical issues found:

  • Multiple locations in manager-embedding-ops.ts and manager-sync-ops.ts access this.provider.id and this.provider.model without null checks, causing runtime errors when sync operations run in FTS-only mode
  • The code will crash when attempting to sync memory files (indexing/embedding operations) with a null provider
  • These crashes would occur even though the search path correctly handles null provider

Test coverage:

  • Added 3 unit tests for FTS-only fallback scenarios in embeddings.test.ts
  • Tests verify null provider is returned with appropriate reasons
  • However, tests don't cover the sync/indexing code paths that have null pointer issues

Confidence Score: 1/5

  • This PR has critical bugs that will cause runtime errors in production
  • While the core concept is sound (enabling FTS-only fallback), the implementation has multiple critical null pointer access bugs in manager-embedding-ops.ts and manager-sync-ops.ts. When sync operations run with a null provider (which can happen in FTS-only mode), the code will crash attempting to access this.provider.id and this.provider.model in at least 8 locations. These are not edge cases - sync is triggered during normal search operations when this.settings.sync.onSearch is enabled.
  • Pay close attention to src/memory/manager-embedding-ops.ts and src/memory/manager-sync-ops.ts - both have multiple null pointer access bugs that need fixing before merge

Last reviewed commit: 9dbb6b3

…aw#17725)

When no embedding provider is available (e.g., OAuth mode without API keys),
memory_search now falls back to FTS-only mode instead of returning disabled: true.

Changes:
- embeddings.ts: return null provider with reason instead of throwing
- manager.ts: handle null provider, use FTS-only search mode
- manager-search.ts: allow searching all models when provider is undefined
- memory-tool.ts: expose search mode in results

The search results now include a 'mode' field indicating 'hybrid' or 'fts-only'.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 files reviewed, 8 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps

greptile-apps Bot commented Feb 16, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (8)

src/memory/manager-sync-ops.ts
accessing this.provider.model and this.provider.id will throw when provider is null (FTS-only mode)

the sync operation can be triggered even in FTS-only mode (when this.provider is null) via search() -> line 213 in manager.ts. this causes a runtime error.

      meta.model !== (this.provider?.model ?? "none") ||
      meta.provider !== (this.provider?.id ?? "none") ||
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-sync-ops.ts
Line: 762:763

Comment:
accessing `this.provider.model` and `this.provider.id` will throw when `provider` is null (FTS-only mode)

the sync operation can be triggered even in FTS-only mode (when `this.provider` is null) via `search()` -> line 213 in `manager.ts`. this causes a runtime error.

```suggestion
      meta.model !== (this.provider?.model ?? "none") ||
      meta.provider !== (this.provider?.id ?? "none") ||
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
accessing this.provider.id and this.provider.model will throw when provider is null (FTS-only mode)

loadEmbeddingCache() can be called indirectly through sync operations even when provider is null. need null checks here.

    const baseParams = [this.provider?.id ?? "none", this.provider?.model ?? "none", this.providerKey];
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 98:98

Comment:
accessing `this.provider.id` and `this.provider.model` will throw when `provider` is null (FTS-only mode)

`loadEmbeddingCache()` can be called indirectly through sync operations even when `provider` is null. need null checks here.

```suggestion
    const baseParams = [this.provider?.id ?? "none", this.provider?.model ?? "none", this.providerKey];
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
accessing this.provider.id and this.provider.model will throw when provider is null

upsertEmbeddingCache() can be called during sync even in FTS-only mode. need null checks.

        this.provider?.id ?? "none",
        this.provider?.model ?? "none",
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 135:136

Comment:
accessing `this.provider.id` and `this.provider.model` will throw when `provider` is null

`upsertEmbeddingCache()` can be called during sync even in FTS-only mode. need null checks.

```suggestion
        this.provider?.id ?? "none",
        this.provider?.model ?? "none",
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
calling enforceEmbeddingMaxInputTokens() with null provider will throw

indexFile() is called during sync operations, which can happen even when provider is null. the function needs a guard:

    const chunks = this.provider
      ? enforceEmbeddingMaxInputTokens(
          this.provider,
          chunkMarkdown(content, this.settings.chunking).filter(
            (chunk) => chunk.text.trim().length > 0,
          ),
        )
      : chunkMarkdown(content, this.settings.chunking).filter(
          (chunk) => chunk.text.trim().length > 0,
        );
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 690:690

Comment:
calling `enforceEmbeddingMaxInputTokens()` with null provider will throw

`indexFile()` is called during sync operations, which can happen even when `provider` is null. the function needs a guard:

```suggestion
    const chunks = this.provider
      ? enforceEmbeddingMaxInputTokens(
          this.provider,
          chunkMarkdown(content, this.settings.chunking).filter(
            (chunk) => chunk.text.trim().length > 0,
          ),
        )
      : chunkMarkdown(content, this.settings.chunking).filter(
          (chunk) => chunk.text.trim().length > 0,
        );
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
accessing this.provider.model will throw when provider is null

          .run(entry.path, options.source, this.provider?.model ?? "none");
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 717:717

Comment:
accessing `this.provider.model` will throw when provider is null

```suggestion
          .run(entry.path, options.source, this.provider?.model ?? "none");
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
accessing this.provider.model will throw when provider is null

        `${options.source}:${entry.path}:${chunk.startLine}:${chunk.endLine}:${chunk.hash}:${this.provider?.model ?? "none"}`,
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 727:728

Comment:
accessing `this.provider.model` will throw when provider is null

```suggestion
        `${options.source}:${entry.path}:${chunk.startLine}:${chunk.endLine}:${chunk.hash}:${this.provider?.model ?? "none"}`,
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
accessing this.provider.model will throw when provider is null

          this.provider?.model ?? "none",
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 747:747

Comment:
accessing `this.provider.model` will throw when provider is null

```suggestion
          this.provider?.model ?? "none",
```

How can I resolve this? If you propose a fix, please make it concise.

src/memory/manager-embedding-ops.ts
accessing this.provider.model will throw when provider is null

            this.provider?.model ?? "none",
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/memory/manager-embedding-ops.ts
Line: 771:771

Comment:
accessing `this.provider.model` will throw when provider is null

```suggestion
            this.provider?.model ?? "none",
```

How can I resolve this? If you propose a fix, please make it concise.

When searching in FTS-only mode (no embedding provider), extract meaningful
keywords from conversational queries to improve search results.

Examples:
- "that thing we discussed about the API" → ["discussed", "API"]
- "之前讨论的那个方案" → ["讨论", "方案"]

Implementation:
- New query-expansion.ts module with local keyword extraction
- Supports English and Chinese stop word filtering
- Searches each keyword separately and merges results
- Falls back to original query if no keywords extracted

This helps users find relevant memory entries even with vague queries,
without requiring an external LLM call (which may not be available in
FTS-only mode due to missing API keys).
@irchelper

Copy link
Copy Markdown
Author

Included in #17800

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory_search always returns disabled when using OAuth (no embedding API key)

1 participant