Skip to content

feat(context): hybrid RRF retrieval backend fusing embedding and fts5#343

Open
Tet-9 wants to merge 1 commit into
vouchdev:testfrom
Tet-9:feat/327-diff
Open

feat(context): hybrid RRF retrieval backend fusing embedding and fts5#343
Tet-9 wants to merge 1 commit into
vouchdev:testfrom
Tet-9:feat/327-diff

Conversation

@Tet-9

@Tet-9 Tet-9 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Closes #316

What

Adds a hybrid retrieval backend that fuses embedding (semantic) and FTS5 (lexical) results using Reciprocal Rank Fusion, surfacing candidates that rank well in either index.

Set retrieval.backend: hybrid in .vouch/config.yaml to activate.

How

  • _retrieve in context.py gains a hybrid branch that calls both index_db.search_semantic and index_db.search at fetch_limit, fuses the results with the existing rrf_fuse primitive in embeddings/fusion.py, then filters and returns with backend tag "hybrid".
  • Degrades gracefully: FTS5-only when no embedding index is present, substring when both indexes are unavailable — same policy as auto.
  • "hybrid" added to _VALID_BACKENDS so it is accepted as a valid config value.
  • kb.capabilities.retrieval already advertises "hybrid" when an embedder is registered (the capabilities() function already had this in place).

Tests added

  • test_hybrid_backend_returns_fused_results — confirms backend tag is "hybrid" and hits are returned
  • test_hybrid_backend_degrades_to_substring_when_no_indexes — no state.db, no embedder; hybrid falls back to substring
  • test_hybrid_backend_deduplicates_items_appearing_in_both_lists — an item ranking in both lists appears exactly once
  • test_hybrid_config_backend_accepted_by_configured_backend_configured_backend returns "hybrid" for the config value

Checklist

  • make check passes (ruff + mypy + pytest, 904 passed — 1 pre-existing unrelated failure in test_capture.py)
  • CHANGELOG.md updated under [Unreleased]
  • Targets test branch
  • No new dependencies (uses existing rrf_fuse in embeddings/fusion.py)

Summary by CodeRabbit

  • New Features

    • Added a new hybrid retrieval mode that combines semantic and keyword search, with automatic fallback when some indexes aren’t available.
    • Expanded OpenClaw support with a new plugin layout and several new slash commands for status, recall, issue resolution, and proposal workflows.
  • Bug Fixes

    • Fixed a crash when RPC responses include date/time values.
    • Improved linting so unreadable source metadata is reported clearly.
  • Documentation

    • Updated setup guides and added tutorials for carrying knowledge across sessions.

@github-actions github-actions Bot added docs documentation, specs, examples, and repo guidance adapters agent host adapters and install manifests openclaw openclaw integration retrieval context, search, synthesis, and evaluation tests tests and fixtures size: L 500-999 changed non-doc lines labels Jul 2, 2026
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This 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.

Changes

Hybrid RRF Retrieval Backend

Layer / File(s) Summary
Hybrid backend implementation
src/vouch/context.py
Adds "hybrid" to _VALID_BACKENDS, imports rrf_fuse, and implements a fused embedding+FTS5 retrieval branch with substring fallback.
Tests and changelog
tests/test_context.py, CHANGELOG.md
Adds tests for fusion, fallback, dedup, and config acceptance; documents the feature.

OpenClaw Plugin Repackaging and RPC Fix

Layer / File(s) Summary
Engine id rename
adapters/openclaw/vouch-context-engine.mjs, src/vouch/openclaw/context_engine.py
Renames ENGINE_ID from vouch-context to vouch and updates docs.
Manifest and package.json restructuring
openclaw.plugin.json, package.json
Rewrites the manifest to the new dialect and adds package.json declaring the extension entry and pluginApi compat.
RPC datetime serialization fix
src/vouch/openclaw/rpc.py, tests/test_openclaw_context_engine.py, CHANGELOG.md
Adds _json_default to serialize datetime/Path in run_stdio responses, with a regression test.
Manifest contract tests
tests/test_openclaw_plugin_manifest.py
Validates id/configSchema, extension entry, skill directory publishing, cross-file parity, dead-field absence.
Real CLI plugin-load e2e tests
tests/test_openclaw_plugin_load_real.py
Installs plugin via real openclaw CLI and validates import, registration, slot binding, skills, doctor.
New skills and docs
adapters/openclaw/skills/*, CLAUDE.md, README.md, adapters/openclaw/README.md
Adds four new skill specs and updates packaging/install documentation.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Lint Unreadable Source Detection

Layer / File(s) Summary
Unreadable source detection
src/vouch/health.py, tests/test_health.py
Adds _collect_sources_for_lint() emitting unreadable_source findings for invalid meta.yaml, with regression test.

Cross-Session Memory Tutorial

Layer / File(s) Summary
Tutorial content and index entry
docs/tutorials/remember-across-sessions.md, docs/tutorials/README.md
Adds a full tutorial and links it from the tutorials index.

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)
Loading
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
Loading

Possibly related issues

Possibly related PRs

  • vouchdev/vouch#38: Adds embedding storage/search functionality that this PR's hybrid backend depends on for semantic candidates.
  • vouchdev/vouch#221: Both modify openclaw.plugin.json schema/manifest fields.
  • vouchdev/vouch#341: Both add the same _collect_sources_for_lint() unreadable_source detection with matching regression tests.

Suggested labels: enhancement

Suggested reviewers: plind-junior

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The core hybrid backend is added, but weighted/rerank support, a shared fusion helper, config docs, and the labeled recall regression test are not shown. Add the missing hybrid knobs and shared fusion path in server/cli/context, update the config template, and include a labeled benchmark proving hybrid recall meets the acceptance criteria.
Out of Scope Changes check ⚠️ Warning The PR also includes unrelated OpenClaw packaging, docs, skills, RPC, and health changes beyond the hybrid retrieval scope. Split unrelated OpenClaw and health changes into separate PRs so this change stays focused on hybrid retrieval.
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: a hybrid context retrieval backend that fuses embedding and FTS5 results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Tet-9 Tet-9 changed the base branch from main to test July 2, 2026 23:03
@github-actions github-actions Bot added size: S 50-199 changed non-doc lines and removed adapters agent host adapters and install manifests openclaw openclaw integration size: L 500-999 changed non-doc lines labels Jul 2, 2026
@Tet-9

Tet-9 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@plind-junior , if you could kindly give this a check.
I believe it's ready for merge, all required factors has ben implemented on it, hope you love it

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 73184a0 and c61ed97.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • CLAUDE.md
  • README.md
  • adapters/openclaw/README.md
  • adapters/openclaw/skills/vouch-propose-from-pr/SKILL.md
  • adapters/openclaw/skills/vouch-recall/SKILL.md
  • adapters/openclaw/skills/vouch-resolve-issue/SKILL.md
  • adapters/openclaw/skills/vouch-status/SKILL.md
  • adapters/openclaw/vouch-context-engine.mjs
  • docs/tutorials/README.md
  • docs/tutorials/remember-across-sessions.md
  • openclaw.plugin.json
  • package.json
  • src/vouch/context.py
  • src/vouch/health.py
  • src/vouch/openclaw/context_engine.py
  • src/vouch/openclaw/rpc.py
  • tests/test_context.py
  • tests/test_health.py
  • tests/test_openclaw_context_engine.py
  • tests/test_openclaw_plugin_load_real.py
  • tests/test_openclaw_plugin_manifest.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between 73184a0 and c61ed97.

📒 Files selected for processing (22)
  • CHANGELOG.md
  • CLAUDE.md
  • README.md
  • adapters/openclaw/README.md
  • adapters/openclaw/skills/vouch-propose-from-pr/SKILL.md
  • adapters/openclaw/skills/vouch-recall/SKILL.md
  • adapters/openclaw/skills/vouch-resolve-issue/SKILL.md
  • adapters/openclaw/skills/vouch-status/SKILL.md
  • adapters/openclaw/vouch-context-engine.mjs
  • docs/tutorials/README.md
  • docs/tutorials/remember-across-sessions.md
  • openclaw.plugin.json
  • package.json
  • src/vouch/context.py
  • src/vouch/health.py
  • src/vouch/openclaw/context_engine.py
  • src/vouch/openclaw/rpc.py
  • tests/test_context.py
  • tests/test_health.py
  • tests/test_openclaw_context_engine.py
  • tests/test_openclaw_plugin_load_real.py
  • tests/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. text is 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
done

Repository: 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.md

Repository: vouchdev/vouch

Length of output: 29274


Migrate existing OpenClaw installs off vouch-context
This rename leaves any existing plugins.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 init and vouch 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 SessionStart hook chain.

the installer wiring also runs vouch capture finalize-all, vouch status, and vouch capture banner before vouch recall on SessionStart. calling out only the recall step makes the setup sound less complete than the generated .claude/settings.json actually 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/lex here are fetched at limit=fetch_limit, but the sibling hybrid implementations in _h_search (jsonl_server.py) and search (cli.py) both fetch each side at limit=fetch_limit * 2 before calling rrf_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 through context.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 configurable fusion, rrf_k, fusion_weights, and rerank knobs; 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 in embeddings/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 Exception around search_semantic masks real bugs, and is inconsistent with sibling call sites.

contextlib.suppress(Exception) on line 104 swallows any exception from search_semantic, not just missing-embedder conditions. index_db.search_semantic already internally handles the "no embedder available" case by catching ImportError/KeyError and returning [] (see the upstream contract snippet). Anything else it raises (e.g. a bug in embedder.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 same search_semantic with 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.

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

Labels

docs documentation, specs, examples, and repo guidance retrieval context, search, synthesis, and evaluation size: S 50-199 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: hybrid rrf retrieval — fuse fts5 and embedding results instead of a fallback chain

1 participant