Skip to content

Commit 5fe349c

Browse files
author
codejunkie99
committed
feat(memory): prefer ripgrep over grep in memory search fallback
Written by Minimax-M2.7 in Claude Code Harness. When SQLite FTS5 is unavailable, memory_search now uses ripgrep (`rg`) if it's on PATH, falling back to grep only when rg is missing. ripgrep is faster, UTF-8 clean by default, and respects gitignore. The .md/.jsonl target restriction from the previous fix is preserved for both tools — implementation files never reach the search path. - Adds _fallback_command() that picks rg vs grep based on PATH, returns (cmd, tool_name). - Adds fallback_tool() surfacing the selected tool in --status. - Renames search_grep to search_fallback; keeps search_grep as a backwards-compat alias. - --status now reports "Mode: FALLBACK (ripgrep|grep|unavailable)" and "Fallback available:" in FTS5 mode too. - README memory_search section mentions ripgrep preference.
1 parent e83f8f7 commit 5fe349c

2 files changed

Lines changed: 52 additions & 18 deletions

File tree

.agent/memory/memory_search.py

Lines changed: 49 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
Memory Search [BETA] — SQLite FTS5 full-text search over .agent/memory/ files.
44
55
Indexes all .md and .jsonl files under .agent/memory/ and provides ranked
6-
keyword search. Falls back to grep (restricted to .md/.jsonl) if FTS5 is
7-
not available.
6+
keyword search. When SQLite FTS5 is not available, falls back to ripgrep
7+
(`rg`) if installed, then to grep. Fallback paths are always restricted
8+
to .md / .jsonl so implementation files never pollute results.
89
910
BETA + opt-in: disabled by default. Enable via onboarding
1011
(agentic-stack <harness> --reconfigure) or by setting
@@ -20,6 +21,7 @@
2021
when any memory file changes, is renamed, or is deleted.
2122
"""
2223
import json
24+
import shutil
2325
import sys
2426
import sqlite3
2527
import subprocess
@@ -176,28 +178,56 @@ def search_fts5(query: str):
176178
return rows
177179

178180

179-
def search_grep(query: str):
180-
"""Fallback search using grep, restricted to memory document files.
181+
def _fallback_command(query, targets):
182+
"""Return (cmd, tool_name) for the best available fallback searcher.
181183
182-
Passing explicit target paths (not the whole directory) ensures we
183-
don't match implementation files like archive.py or auto_dream.py —
184-
keyword retrieval must only surface .md / .jsonl memory content.
184+
Prefers ripgrep (faster, UTF-8 clean, sensible defaults). Falls back
185+
to grep for POSIX environments. Returns (None, None) if neither is
186+
on PATH — callers should degrade gracefully.
187+
"""
188+
if shutil.which("rg"):
189+
# -l: files-with-matches, -i: case-insensitive, -- ends flags
190+
return (["rg", "-li", "--", query, *targets], "ripgrep")
191+
if shutil.which("grep"):
192+
return (["grep", "-ril", query, *targets], "grep")
193+
return (None, None)
194+
195+
196+
def fallback_tool():
197+
"""Name of the external tool that would be used for fallback search.
198+
199+
'ripgrep' if rg is on PATH, else 'grep' if grep is, else 'unavailable'.
200+
Surfaced in --status so users know what mode a query would run in.
201+
"""
202+
_, tool = _fallback_command("", [])
203+
return tool or "unavailable"
204+
205+
206+
def search_fallback(query: str):
207+
"""Full-text search without FTS5, restricted to memory document files.
208+
209+
Passing explicit target paths (not the whole directory) keeps
210+
keyword retrieval scoped to .md / .jsonl — implementation files
211+
like archive.py or auto_dream.py never pollute the results.
185212
"""
186213
targets = [str(f) for f in _memory_files()]
187214
if not targets:
188215
return []
189-
result = subprocess.run(
190-
["grep", "-ril", query, *targets],
191-
capture_output=True,
192-
text=True,
193-
)
216+
cmd, _ = _fallback_command(query, targets)
217+
if not cmd:
218+
return []
219+
result = subprocess.run(cmd, capture_output=True, text=True)
194220
files = [f for f in result.stdout.strip().split("\n") if f]
195221
return [
196222
(Path(f).relative_to(MEMORY_DIR), f"(match in {Path(f).name})")
197223
for f in files
198224
]
199225

200226

227+
# Backwards-compat alias — anything calling search_grep keeps working.
228+
search_grep = search_fallback
229+
230+
201231
def cmd_rebuild():
202232
if not check_fts5():
203233
print("FTS5 not available — cannot build index.")
@@ -215,8 +245,11 @@ def cmd_status():
215245
print("Or edit .agent/memory/.features.json directly.")
216246
return
217247
if not check_fts5():
218-
print("Mode: BASIC (grep fallback)")
248+
tool = fallback_tool()
249+
print(f"Mode: FALLBACK ({tool})")
219250
print("Reason: SQLite FTS5 not available in this Python build.")
251+
if tool == "unavailable":
252+
print("Also: neither rg nor grep on PATH — install ripgrep for best results.")
220253
return
221254
if not INDEX_PATH.exists():
222255
print("Mode: FTS5 (index not built yet — auto-builds on first search)")
@@ -228,6 +261,7 @@ def cmd_status():
228261
print(f"Mode: FTS5")
229262
print(f"Index: {count} files indexed ({size_kb} KB)")
230263
print(f"Location: {INDEX_PATH}")
264+
print(f"Fallback available: {fallback_tool()}")
231265

232266

233267
def _refuse_disabled():
@@ -271,8 +305,8 @@ def main():
271305
results = search_fts5(query)
272306
mode = "FTS5"
273307
else:
274-
results = search_grep(query)
275-
mode = "grep"
308+
results = search_fallback(query)
309+
mode = fallback_tool()
276310

277311
if not results:
278312
print(f"No results for: '{query}' [mode: {mode}]")

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,9 @@ python3 .agent/memory/memory_search.py --status
173173
python3 .agent/memory/memory_search.py --rebuild
174174
```
175175

176-
Falls back to `grep` (restricted to `.md` / `.jsonl`) when SQLite FTS5
177-
isn't available. The index is stored at `.agent/memory/.index/` and
178-
gitignored.
176+
Falls back to **ripgrep** (`rg`) if installed, then to `grep` — both
177+
restricted to `.md` / `.jsonl` so source files never pollute results.
178+
The index is stored at `.agent/memory/.index/` and gitignored.
179179

180180
## Repo layout
181181

0 commit comments

Comments
 (0)