fix(memory): populate FTS index when no embedding provider is configured#56473
Conversation
Greptile SummaryThis PR correctly fixes the core bug where FTS indexing was entirely skipped when no embedding provider was configured. It removes the provider-null early returns from One P1 regression was found:
The stale-file deletion in Confidence Score: 4/5Core FTS-only indexing fix is correct, but a P1 regression causes stale FTS rows to accumulate on incremental file re-indexing in FTS-only mode — should be addressed before merging. The primary fix is well-reasoned and the new reindex-transition detection is correct. However, clearIndexedFileData retains a && this.provider guard that prevents FTS cleanup during incremental re-indexing in FTS-only mode, resulting in stale rows in chunks_fts that will cause incorrect search results after any file modification in a provider-less setup. This is a real defect on the changed path that is not covered by the new tests. extensions/memory-core/src/memory/manager-embedding-ops.ts — specifically the clearIndexedFileData method at line 556.
|
| Filename | Overview |
|---|---|
| extensions/memory-core/src/memory/manager-embedding-ops.ts | Extracts a writeChunks helper and adds a correct FTS-only branch to indexFile. However, clearIndexedFileData still guards FTS row deletion with && this.provider, which will cause stale FTS rows to accumulate when files are modified and re-indexed incrementally in FTS-only mode. |
| extensions/memory-core/src/memory/manager-sync-ops.ts | Removes the early-return provider guards from syncMemoryFiles and syncSessionFiles, fixes stale-file FTS deletion to use this.provider?.model ?? 'fts-only', and adds needsFullReindex detection for provider→FTS-only transitions. Logic looks correct. |
| extensions/memory-core/src/memory/index.test.ts | Adds two well-structured integration tests covering the FTS-only indexing path and the provider→FTS-only reindex transition. Coverage is good, though the incremental-sync-after-file-modification path in FTS-only mode is not covered. |
Comments Outside Diff (1)
-
extensions/memory-core/src/memory/manager-embedding-ops.ts, line 556-562 (link)Stale FTS rows not deleted on incremental re-index in FTS-only mode
clearIndexedFileDataguards FTS deletion with&& this.provider. In FTS-only mode (this.provider === null), when a file's content changes and is re-indexed incrementally (needsFullReindex = false), the old FTS rows are silently left inchunks_ftsbefore new ones are inserted.The sequence:
- File indexed → FTS rows written with chunk IDs derived from the old content hash.
- File modified → next incremental sync calls
writeChunks→clearIndexedFileDatais called. clearIndexedFileDataskips FTS deletion (guard&& this.provideris false), but does delete from thechunkstable.- New chunks + new FTS rows are inserted.
chunks_ftsnow contains both the orphaned old rows (no matchingchunksentry) and the new rows — subsequent keyword searches can return results pointing at deleted or outdated content.
The fix is to drop the
this.providerguard and usethis.provider?.model ?? "fts-only"as the model value, consistent with how stale-file deletion already works insyncMemoryFiles/syncSessionFiles:if (this.fts.enabled && this.fts.available) { const ftsModel = this.provider?.model ?? "fts-only"; try { this.db .prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`) .run(pathname, source, ftsModel); } catch {} }
Prompt To Fix With AI
This is a comment left during a code review. Path: extensions/memory-core/src/memory/manager-embedding-ops.ts Line: 556-562 Comment: **Stale FTS rows not deleted on incremental re-index in FTS-only mode** `clearIndexedFileData` guards FTS deletion with `&& this.provider`. In FTS-only mode (`this.provider === null`), when a file's content changes and is re-indexed incrementally (`needsFullReindex = false`), the old FTS rows are silently left in `chunks_fts` before new ones are inserted. The sequence: 1. File indexed → FTS rows written with chunk IDs derived from the old content hash. 2. File modified → next incremental sync calls `writeChunks` → `clearIndexedFileData` is called. 3. `clearIndexedFileData` **skips** FTS deletion (guard `&& this.provider` is false), but **does** delete from the `chunks` table. 4. New chunks + new FTS rows are inserted. 5. `chunks_fts` now contains both the orphaned old rows (no matching `chunks` entry) and the new rows — subsequent keyword searches can return results pointing at deleted or outdated content. The fix is to drop the `this.provider` guard and use `this.provider?.model ?? "fts-only"` as the model value, consistent with how stale-file deletion already works in `syncMemoryFiles`/`syncSessionFiles`: ```typescript if (this.fts.enabled && this.fts.available) { const ftsModel = this.provider?.model ?? "fts-only"; try { this.db .prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`) .run(pathname, source, ftsModel); } catch {} } ``` How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/memory-core/src/memory/manager-embedding-ops.ts
Line: 556-562
Comment:
**Stale FTS rows not deleted on incremental re-index in FTS-only mode**
`clearIndexedFileData` guards FTS deletion with `&& this.provider`. In FTS-only mode (`this.provider === null`), when a file's content changes and is re-indexed incrementally (`needsFullReindex = false`), the old FTS rows are silently left in `chunks_fts` before new ones are inserted.
The sequence:
1. File indexed → FTS rows written with chunk IDs derived from the old content hash.
2. File modified → next incremental sync calls `writeChunks` → `clearIndexedFileData` is called.
3. `clearIndexedFileData` **skips** FTS deletion (guard `&& this.provider` is false), but **does** delete from the `chunks` table.
4. New chunks + new FTS rows are inserted.
5. `chunks_fts` now contains both the orphaned old rows (no matching `chunks` entry) and the new rows — subsequent keyword searches can return results pointing at deleted or outdated content.
The fix is to drop the `this.provider` guard and use `this.provider?.model ?? "fts-only"` as the model value, consistent with how stale-file deletion already works in `syncMemoryFiles`/`syncSessionFiles`:
```typescript
if (this.fts.enabled && this.fts.available) {
const ftsModel = this.provider?.model ?? "fts-only";
try {
this.db
.prepare(`DELETE FROM ${FTS_TABLE} WHERE path = ? AND source = ? AND model = ?`)
.run(pathname, source, ftsModel);
} catch {}
}
```
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "fix(memory): trigger full reindex on pro..." | Re-trigger Greptile
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 82a8626875
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
82a8626 to
2a42cbb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2a42cbb3b2
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
8e4e7e7 to
2113520
Compare
2113520 to
896b655
Compare
|
Addressed the default-threshold gap in provider-less search. Change:
Verification:
|
896b655 to
c6eb4bf
Compare
obviyus
left a comment
There was a problem hiding this comment.
Reviewed latest changes; landing now.
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
… (thanks @opriz) * fix(memory): build FTS index when no embedding provider is available * fix(memory): trigger full reindex on provider→FTS-only transition * fix(memory): return FTS-only keyword hits at default threshold * fix: keep FTS-only memory hits at default threshold (openclaw#56473) (thanks @opriz) --------- Co-authored-by: Ayaan Zaidi <[email protected]>
Summary
syncMemoryFiles()andsyncSessionFiles()returned early unconditionally — skipping not just embedding generation but also FTS indexing. The FTS5 table (chunks_fts) was never populated, even though FTS5 is a built-in SQLite feature that requires no external dependency whatsoever. The default search path already includes a FTS-only branch that callssearchKeyword()when no provider is available, but because the index was never built, it always returned empty results.memory_searchand gets nothing back — effective amnesia with no warning.indexFile()now has a dedicated FTS-only branch that chunks files and writes tochunks+chunks_fts(model sentinel"fts-only", empty embeddings, no vector writes). Also fixedneedsFullReindexto detect provider→FTS-only transitions so orphaned old-model FTS rows are cleaned up on the next sync. Extracted awriteChunks()helper to eliminate duplicated three-table write logic between the two paths.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause / Regression History (if applicable)
if (!this.provider) return;guards insyncMemoryFiles(),syncSessionFiles(), andindexFile()were intended to skip embedding generation but inadvertently skipped FTS writes too. The comment said "FTS-only mode" but the code skipped FTS as well.manager.ts:346-391) already had a correct FTS-only branch callingsearchKeyword()— but the index was always empty, so it returned nothing.Regression Test Plan (if applicable)
extensions/memory-core/src/memory/index.test.ts"builds FTS index and returns search results when no embedding provider is available"— verifieschunks > 0,embedBatchCalls === 0, keyword search returns results, unknown terms return empty."triggers full reindex and cleans up old-model FTS rows when switching from provider to FTS-only"— verifies oldmodel = "mock-embed"FTS rows are removed and newmodel = "fts-only"rows exist after transition.User-visible / Behavior Changes
openclaw memory statuswill showchunks > 0andFTS: readyin provider-less setups.Diagram (if applicable)
Security Impact (required)
Repro + Verification
Environment
agents.defaults.memorySearch.provider = "auto",query.hybrid.enabled = trueSteps
provider: "auto"with no keys)openclaw memory index --forceopenclaw memory statusopenclaw memory search "<keyword>" --min-score 0Expected
statusshowsIndexed: N files · M chunks,FTS: readysearchreturns matching resultsActual (before fix)
statusshowsIndexed: 0/N files · 0 chunkssearchreturnsNo matchesActual (after fix)
statusshowsIndexed: 6/6 files · 35 chunks,FTS: readysearch --min-score 0returns matching chunksEvidence
Human Verification (required)
openclaw memory index+openclaw memory status+openclaw memory searchon a local instance withProvider: none (requested: auto)— confirmed35 chunksindexed and keyword search returning results with--min-score 0.Review Conversations
Compatibility / Migration
Risks and Mitigations
minScore = 0.35.--min-score 0. Not introduced by this PR — the search path already existed, just had no data.