feat(context): hybrid RRF retrieval backend fusing embedding and fts5#343
feat(context): hybrid RRF retrieval backend fusing embedding and fts5#343Tet-9 wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis PR adds a hybrid RRF retrieval backend to context.py, repackages the OpenClaw plugin for the 2026.6 loader (renamed engine id, restructured manifest, new package.json), fixes a datetime JSON serialization crash in openclaw-rpc, adds unreadable-source lint detection in health.py, and adds new skills/tutorial documentation. ChangesHybrid RRF Retrieval Backend
OpenClaw Plugin Repackaging and RPC Fix
Estimated code review effort: 3 (Moderate) | ~30 minutes Lint Unreadable Source Detection
Cross-Session Memory Tutorial
Sequence Diagram(s)sequenceDiagram
participant Caller
participant _retrieve
participant EmbeddingIndex
participant FTS5Index
participant rrf_fuse
Caller->>_retrieve: query(backend="hybrid")
_retrieve->>EmbeddingIndex: search_semantic(query)
_retrieve->>FTS5Index: search(query)
EmbeddingIndex-->>_retrieve: semantic candidates
FTS5Index-->>_retrieve: lexical candidates
_retrieve->>rrf_fuse: fuse(semantic, lexical)
rrf_fuse-->>_retrieve: fused ranked hits
_retrieve-->>Caller: results tagged "hybrid" (or "substring" fallback)
sequenceDiagram
participant Stdin
participant run_stdio
participant handle_request
participant json_dumps as "json.dumps"
Stdin->>run_stdio: JSON request envelope
run_stdio->>handle_request: process request
handle_request-->>run_stdio: response (contains datetime generated_at)
run_stdio->>json_dumps: dumps(response, default=_json_default)
json_dumps-->>run_stdio: serialized JSON string
run_stdio-->>Stdin: write response
Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@plind-junior , if you could kindly give this a check. |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@adapters/openclaw/skills/vouch-status/SKILL.md`:
- Around line 14-21: The markdown example in SKILL.md uses a fenced code block
without a language tag, which markdownlint will flag. Update the example fence
around the “KB at <root>” output snippet in the skill document to use a text
language tag, keeping the content unchanged while making the fenced block
explicit.
In `@adapters/openclaw/vouch-context-engine.mjs`:
- Around line 4-21: The `ENGINE_ID` rename in `vouch-context-engine.mjs` leaves
existing installs with `plugins.slots.contextEngine: "vouch-context"` pointing
to nothing, so add a migration or compatibility shim in the installer/doctor
path so old configs are rewritten to `vouch`. Update the relevant
startup/upgrade handling around the context engine resolution path to recognize
the legacy value and map it to the new plugin id instead of silently falling
back. If code changes are not enough, make sure the install/doctor docs and
changelog explicitly tell users how to upgrade the `contextEngine` slot value.
In `@docs/tutorials/remember-across-sessions.md`:
- Around line 45-51: The tutorial summary omits part of the SessionStart hook
chain, making the installer wiring sound incomplete. Update the wording around
.claude/settings.json and SessionStart to mention the full sequence used there:
vouch capture finalize-all, vouch status, and vouch capture banner before vouch
recall. Keep the references to PostToolUse and SessionEnd intact, and ensure the
description of the hook flow matches the actual installer behavior.
- Around line 13-17: The wording in the tutorial overstates that “the only vouch
command you ever type is the 30-second review,” which conflicts with the setup
flow that uses vouch init and vouch install-mcp claude-code. Update the copy in
remember-across-sessions.md to narrow the claim by referring to the post-setup
workflow, using the surrounding session-approval paragraph as the anchor, so it
reads like “after setup, the only vouch command in the workflow is approve.”
In `@src/vouch/context.py`:
- Around line 91-117: The hybrid search path in context.py is drifting from the
CLI/jsonl_server behavior because it fetches semantic and lexical candidates at
fetch_limit instead of the deeper fetch_limit * 2 used elsewhere, and the fusion
workflow is duplicated across three call sites. Update the hybrid branch in the
context search flow to use the same fetch depth and call pattern as _h_search in
jsonl_server.py and search in cli.py, so rrf_fuse gets a comparable candidate
pool. Then extract the shared sem/lex fetch + suppress + fuse logic into one
helper (for example, a hybrid_search helper in embeddings/fusion.py) and have
Context search, the server, and the CLI all reuse it.
- Around line 103-108: Replace the broad exception suppression around
search_semantic in context.py with handling only the expected missing-embedder
failure modes already covered by index_db.search_semantic itself. Keep the
hybrid search path in the same failure contract as the later semantic branch by
removing contextlib.suppress(Exception) and letting real bugs from
search_semantic/embedder.encode surface; if you still need fallback behavior,
catch only the documented ImportError/KeyError cases at the search_semantic call
site. Refer to the search_semantic and search calls in the semantic/lexical
merge block so the behavior stays consistent with the “auto”/“embedding” path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c1462689-debf-4e5e-bf5d-ff0e79452ad2
📒 Files selected for processing (22)
CHANGELOG.mdCLAUDE.mdREADME.mdadapters/openclaw/README.mdadapters/openclaw/skills/vouch-propose-from-pr/SKILL.mdadapters/openclaw/skills/vouch-recall/SKILL.mdadapters/openclaw/skills/vouch-resolve-issue/SKILL.mdadapters/openclaw/skills/vouch-status/SKILL.mdadapters/openclaw/vouch-context-engine.mjsdocs/tutorials/README.mddocs/tutorials/remember-across-sessions.mdopenclaw.plugin.jsonpackage.jsonsrc/vouch/context.pysrc/vouch/health.pysrc/vouch/openclaw/context_engine.pysrc/vouch/openclaw/rpc.pytests/test_context.pytests/test_health.pytests/test_openclaw_context_engine.pytests/test_openclaw_plugin_load_real.pytests/test_openclaw_plugin_manifest.py
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@adapters/openclaw/skills/vouch-status/SKILL.md`:
- Around line 14-21: The markdown example in SKILL.md uses a fenced code block
without a language tag, which markdownlint will flag. Update the example fence
around the “KB at <root>” output snippet in the skill document to use a text
language tag, keeping the content unchanged while making the fenced block
explicit.
In `@adapters/openclaw/vouch-context-engine.mjs`:
- Around line 4-21: The `ENGINE_ID` rename in `vouch-context-engine.mjs` leaves
existing installs with `plugins.slots.contextEngine: "vouch-context"` pointing
to nothing, so add a migration or compatibility shim in the installer/doctor
path so old configs are rewritten to `vouch`. Update the relevant
startup/upgrade handling around the context engine resolution path to recognize
the legacy value and map it to the new plugin id instead of silently falling
back. If code changes are not enough, make sure the install/doctor docs and
changelog explicitly tell users how to upgrade the `contextEngine` slot value.
In `@docs/tutorials/remember-across-sessions.md`:
- Around line 45-51: The tutorial summary omits part of the SessionStart hook
chain, making the installer wiring sound incomplete. Update the wording around
.claude/settings.json and SessionStart to mention the full sequence used there:
vouch capture finalize-all, vouch status, and vouch capture banner before vouch
recall. Keep the references to PostToolUse and SessionEnd intact, and ensure the
description of the hook flow matches the actual installer behavior.
- Around line 13-17: The wording in the tutorial overstates that “the only vouch
command you ever type is the 30-second review,” which conflicts with the setup
flow that uses vouch init and vouch install-mcp claude-code. Update the copy in
remember-across-sessions.md to narrow the claim by referring to the post-setup
workflow, using the surrounding session-approval paragraph as the anchor, so it
reads like “after setup, the only vouch command in the workflow is approve.”
In `@src/vouch/context.py`:
- Around line 91-117: The hybrid search path in context.py is drifting from the
CLI/jsonl_server behavior because it fetches semantic and lexical candidates at
fetch_limit instead of the deeper fetch_limit * 2 used elsewhere, and the fusion
workflow is duplicated across three call sites. Update the hybrid branch in the
context search flow to use the same fetch depth and call pattern as _h_search in
jsonl_server.py and search in cli.py, so rrf_fuse gets a comparable candidate
pool. Then extract the shared sem/lex fetch + suppress + fuse logic into one
helper (for example, a hybrid_search helper in embeddings/fusion.py) and have
Context search, the server, and the CLI all reuse it.
- Around line 103-108: Replace the broad exception suppression around
search_semantic in context.py with handling only the expected missing-embedder
failure modes already covered by index_db.search_semantic itself. Keep the
hybrid search path in the same failure contract as the later semantic branch by
removing contextlib.suppress(Exception) and letting real bugs from
search_semantic/embedder.encode surface; if you still need fallback behavior,
catch only the documented ImportError/KeyError cases at the search_semantic call
site. Refer to the search_semantic and search calls in the semantic/lexical
merge block so the behavior stays consistent with the “auto”/“embedding” path.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c1462689-debf-4e5e-bf5d-ff0e79452ad2
📒 Files selected for processing (22)
CHANGELOG.mdCLAUDE.mdREADME.mdadapters/openclaw/README.mdadapters/openclaw/skills/vouch-propose-from-pr/SKILL.mdadapters/openclaw/skills/vouch-recall/SKILL.mdadapters/openclaw/skills/vouch-resolve-issue/SKILL.mdadapters/openclaw/skills/vouch-status/SKILL.mdadapters/openclaw/vouch-context-engine.mjsdocs/tutorials/README.mddocs/tutorials/remember-across-sessions.mdopenclaw.plugin.jsonpackage.jsonsrc/vouch/context.pysrc/vouch/health.pysrc/vouch/openclaw/context_engine.pysrc/vouch/openclaw/rpc.pytests/test_context.pytests/test_health.pytests/test_openclaw_context_engine.pytests/test_openclaw_plugin_load_real.pytests/test_openclaw_plugin_manifest.py
🛑 Comments failed to post (6)
adapters/openclaw/skills/vouch-status/SKILL.md (1)
14-21: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
add a language tag to the example fence.
markdownlint will flag this block because the fenced example has no language.
textis enough for an output sample.♻️ proposed fix
-``` +```text📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 14-14: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@adapters/openclaw/skills/vouch-status/SKILL.md` around lines 14 - 21, The markdown example in SKILL.md uses a fenced code block without a language tag, which markdownlint will flag. Update the example fence around the “KB at <root>” output snippet in the skill document to use a text language tag, keeping the content unchanged while making the fenced block explicit.Source: Linters/SAST tools
adapters/openclaw/vouch-context-engine.mjs (1)
4-21: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n== Files of interest ==\n' git ls-files | rg '(^|/)(openclaw|context|doctor|install|plugin).*|CHANGELOG|package.json|openclaw\.plugin\.json|openclaw\.json|vouch-context-engine\.mjs$' printf '\n== Search legacy and current engine ids ==\n' rg -n --hidden --no-ignore-vcs '"vouch-context"|"vouch"|contextEngine|ENGINE_ID|plugin id mismatch|legacy engine|doctor --fix|auto-migrate' . printf '\n== Candidate file outlines ==\n' for f in adapters/openclaw/vouch-context-engine.mjs openclaw.plugin.json openclaw.json package.json CHANGELOG.md; do if [ -f "$f" ]; then echo "--- $f ---" wc -l "$f" ast-grep outline "$f" --view expanded || true fi doneRepository: vouchdev/vouch
Length of output: 10908
🏁 Script executed:
#!/bin/bash set -euo pipefail printf '\n== OpenClaw engine file ==\n' cat -n adapters/openclaw/vouch-context-engine.mjs | sed -n '1,140p' printf '\n== OpenClaw adapter docs and install ==\n' for f in adapters/openclaw/install.yaml adapters/openclaw/README.md openclaw.plugin.json CHANGELOG.md; do if [ -f "$f" ]; then echo "--- $f ---" cat -n "$f" | sed -n '1,220p' fi done printf '\n== Search for migration / legacy slot handling ==\n' rg -n --hidden --no-ignore-vcs 'vouch-context|contextEngine.*(migrate|migration|rename|legacy|auto-bind|doctor)|plugins\.slots\.contextEngine|doctor --fix|openclaw.*fix' adapters src docs tests CHANGELOG.md CLAUDE.mdRepository: vouchdev/vouch
Length of output: 29274
Migrate existing OpenClaw installs off
vouch-context
This rename leaves any existingplugins.slots.contextEngine: "vouch-context"config pointing at nothing, so upgrades will silently fall back to the legacy engine until users edit it manually. Add a migration in the installer/doctor --fix, or at least surface a clear upgrade path in the docs/changelog.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@adapters/openclaw/vouch-context-engine.mjs` around lines 4 - 21, The `ENGINE_ID` rename in `vouch-context-engine.mjs` leaves existing installs with `plugins.slots.contextEngine: "vouch-context"` pointing to nothing, so add a migration or compatibility shim in the installer/doctor path so old configs are rewritten to `vouch`. Update the relevant startup/upgrade handling around the context engine resolution path to recognize the legacy value and map it to the new plugin id instead of silently falling back. If code changes are not enough, make sure the install/doctor docs and changelog explicitly tell users how to upgrade the `contextEngine` slot value.docs/tutorials/remember-across-sessions.md (2)
13-17: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
narrow the “only vouch command” claim.
this reads as if the user never types any vouch command except approve, but the setup flow still uses
vouch initandvouch install-mcp claude-code. rewording it to “after setup, the only vouch command in the workflow is approve” would avoid confusing readers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/tutorials/remember-across-sessions.md` around lines 13 - 17, The wording in the tutorial overstates that “the only vouch command you ever type is the 30-second review,” which conflicts with the setup flow that uses vouch init and vouch install-mcp claude-code. Update the copy in remember-across-sessions.md to narrow the claim by referring to the post-setup workflow, using the surrounding session-approval paragraph as the anchor, so it reads like “after setup, the only vouch command in the workflow is approve.”
45-51: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
mention the full
SessionStarthook chain.the installer wiring also runs
vouch capture finalize-all,vouch status, andvouch capture bannerbeforevouch recallonSessionStart. calling out only the recall step makes the setup sound less complete than the generated.claude/settings.jsonactually is.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/tutorials/remember-across-sessions.md` around lines 45 - 51, The tutorial summary omits part of the SessionStart hook chain, making the installer wiring sound incomplete. Update the wording around .claude/settings.json and SessionStart to mention the full sequence used there: vouch capture finalize-all, vouch status, and vouch capture banner before vouch recall. Keep the references to PostToolUse and SessionEnd intact, and ensure the description of the hook flow matches the actual installer behavior.src/vouch/context.py (2)
91-117: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Hybrid fetch depth drifts from the CLI/jsonl_server hybrid implementations, and the fusion logic itself is duplicated 3x.
sem/lexhere are fetched atlimit=fetch_limit, but the sibling hybrid implementations in_h_search(jsonl_server.py) andsearch(cli.py) both fetch each side atlimit=fetch_limit * 2before callingrrf_fuse(..., limit=fetch_limit), explicitly to give the fusion enough candidate depth for meaningful overlap. Using a shallower pool here means the same query/config can produce a different fused ranking depending on whether it's served throughcontext.py, the JSONL server, or the CLI — exactly the kind of drift the linked issue calls out. The linked issue also asks for a single shared fusion helper (used by context/server/CLI) plus configurablefusion,rrf_k,fusion_weights, andrerankknobs; none of that exists yet — instead there are three independent copies of "fetch sem, fetch lex, suppress errors, call rrf_fuse" that have already diverged (this fetch-limit multiplier being the proof).Recommend extracting a single
hybrid_search(kb_dir, query, *, limit, min_score=0.0, k=60)helper inembeddings/fusion.py(or similar) that all three call sites use, so the fetch-depth, error handling, and future knobs stay in one place.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/vouch/context.py` around lines 91 - 117, The hybrid search path in context.py is drifting from the CLI/jsonl_server behavior because it fetches semantic and lexical candidates at fetch_limit instead of the deeper fetch_limit * 2 used elsewhere, and the fusion workflow is duplicated across three call sites. Update the hybrid branch in the context search flow to use the same fetch depth and call pattern as _h_search in jsonl_server.py and search in cli.py, so rrf_fuse gets a comparable candidate pool. Then extract the shared sem/lex fetch + suppress + fuse logic into one helper (for example, a hybrid_search helper in embeddings/fusion.py) and have Context search, the server, and the CLI all reuse it.
103-108: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Broad
except Exceptionaroundsearch_semanticmasks real bugs, and is inconsistent with sibling call sites.
contextlib.suppress(Exception)on line 104 swallows any exception fromsearch_semantic, not just missing-embedder conditions.index_db.search_semanticalready internally handles the "no embedder available" case by catchingImportError/KeyErrorand returning[](see the upstream contract snippet). Anything else it raises (e.g. a bug inembedder.encode()) is a real defect — yet here it's silently treated as "no semantic hits" and the hybrid path falls through to FTS5/substring without any signal. Compare this to the"auto"/"embedding"branch a few lines below (Line 119), which calls the exact samesearch_semanticwith no exception handling at all, so the identical underlying bug would crash there instead. That's an inconsistent failure contract for the same function depending on which backend is configured.🛡️ Narrow the suppressed exceptions to match the documented failure modes
sem: list[tuple[str, str, str, float]] = [] - with contextlib.suppress(Exception): - sem = index_db.search_semantic(store.kb_dir, query, limit=fetch_limit) or [] + with contextlib.suppress(ImportError, KeyError): + sem = index_db.search_semantic(store.kb_dir, query, limit=fetch_limit) or []📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.sem: list[tuple[str, str, str, float]] = [] with contextlib.suppress(ImportError, KeyError): sem = index_db.search_semantic(store.kb_dir, query, limit=fetch_limit) or [] lex: list[tuple[str, str, str, float]] = [] with contextlib.suppress(sqlite3.Error): lex = index_db.search(store.kb_dir, query, limit=fetch_limit) or []🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/vouch/context.py` around lines 103 - 108, Replace the broad exception suppression around search_semantic in context.py with handling only the expected missing-embedder failure modes already covered by index_db.search_semantic itself. Keep the hybrid search path in the same failure contract as the later semantic branch by removing contextlib.suppress(Exception) and letting real bugs from search_semantic/embedder.encode surface; if you still need fallback behavior, catch only the documented ImportError/KeyError cases at the search_semantic call site. Refer to the search_semantic and search calls in the semantic/lexical merge block so the behavior stays consistent with the “auto”/“embedding” path.
Closes #316
What
Adds a
hybridretrieval backend that fuses embedding (semantic) and FTS5 (lexical) results using Reciprocal Rank Fusion, surfacing candidates that rank well in either index.Set
retrieval.backend: hybridin.vouch/config.yamlto activate.How
_retrieveincontext.pygains ahybridbranch that calls bothindex_db.search_semanticandindex_db.searchatfetch_limit, fuses the results with the existingrrf_fuseprimitive inembeddings/fusion.py, then filters and returns with backend tag"hybrid".auto."hybrid"added to_VALID_BACKENDSso it is accepted as a valid config value.kb.capabilities.retrievalalready advertises"hybrid"when an embedder is registered (thecapabilities()function already had this in place).Tests added
test_hybrid_backend_returns_fused_results— confirms backend tag is"hybrid"and hits are returnedtest_hybrid_backend_degrades_to_substring_when_no_indexes— no state.db, no embedder; hybrid falls back to substringtest_hybrid_backend_deduplicates_items_appearing_in_both_lists— an item ranking in both lists appears exactly oncetest_hybrid_config_backend_accepted_by_configured_backend—_configured_backendreturns"hybrid"for the config valueChecklist
make checkpasses (ruff + mypy + pytest, 904 passed — 1 pre-existing unrelated failure in test_capture.py)CHANGELOG.mdupdated under[Unreleased]testbranchrrf_fuseinembeddings/fusion.py)Summary by CodeRabbit
New Features
Bug Fixes
Documentation