fix(mcp): add stable semantic pagination - #938
Conversation
|
Thanks for the semantic_query fix for #915. Triage: high-priority query correctness bug. Review should verify semantic-only calls put vector-ranked hits in the primary results array, combined structural+semantic behavior stays compatible, and total/has_more report the semantic candidate set rather than the broad structural fallback. |
|
The shape of this is right: one semantic-query collection feeding either the primary One change requested: the refactor quietly switched With that addressed this is merge-ready. |
|
Implemented your preference and restored yyjson_mut_obj_add_strcpy for all four vector-result strings. |
|
Thanks @dpersek — reviewed in depth. The core fix is right and we want it: a semantic-only Two changes before we can merge, both about not shipping a scale/contract foot-gun:
Also please rebase onto current |
4d14460 to
0850901
Compare
|
Reviewed properly at last — apologies this took so long, because the delay is most of the problem here. Your diagnosis of #915 was exactly right, and it is worth saying plainly: the unfiltered structural search being prepended, letting vector-less nodes win the ranking alphabetically, is precisely the bug. Main's default-path isolation follows the direction this PR established, and it is live today at What happened is that main moved out from under you, twice. Since your last rebase in mid-July, TOON was pruned in favour of the tree format, and then Two pieces here are still genuinely valuable and are not on main:
Both would make a strong focused follow-up against current main. Two things I would want split out and decided explicitly, because they cut results rather than reorder them:
Neither is wrong; both trade recall for precision, and that is a product call we want to make deliberately rather than inherit. Splitting them out lets each be judged on its own. Also riding along undeclared: a store-API signature change ( Suggested path: let #1319 take the #1295 JSON residual, and open a focused PR against current main with the pagination and deterministic ordering. If you would rather not carry that yourself, we can distill those two pieces with One correction I owe you for accuracy: I initially attributed main's isolation to a different commit while reviewing this. The correct one is |
|
Thanks for the thorough review and for spelling out how main moved. I’d like to carry the focused follow-up. I’ll base it on current main, leave #1319’s isolation work alone, and limit it to deterministic ordering plus pagination metadata. One implementation question before I start: should pagination be defined within the existing bounded |
|
Sorry for the wait on this — you asked a good question and then heard nothing. Short answer: a fixed candidate window, not The problem with
A fixed window gives you one stable candidate set per query, which makes So: fetch a fixed number of ranked candidates, sort deterministically (score DESC, On the window size itself — you're right to treat it as a decision rather than an accident, and I'd rather it were explicit in the PR than inherited. My suggestion is to state it as a named constant with a comment saying what it costs and what it buys, and to document plainly in the tool's response semantics that results are ranked within a bounded window rather than the full corpus. 250 (what your earlier branch used) is a reasonable starting point; if you have a view from having profiled the vector scan, I'd weight that over my guess. What matters most is that the number is visible and justified in one place, not that it is any particular value. And please make the truncation legible. If a query fills the window, the response should say so rather than just setting Leaving the score threshold out of this PR is the right call — agreed, and thank you for splitting it. Ping me when the PR is up and I'll review it promptly this time rather than letting it sit. Your original diagnosis on #915 was correct and it deserved a faster turnaround than it got. |
|
@dpersek — a ping and a status change, with thanks attached. Converted to draft: until the focused follow-up is ready, this is honestly a draft — the headline fix you drove already shipped on And the thanks, because the record deserves it: you diagnosed #915 correctly, pushed the design conversation until it converged, and then — when main moved twice under your branch — chose the harder, better path of a ground-up rework over a patched rebase, declining the offered distillation to keep the work yours. The shipped fix exists because you kept at it. The draft state means "awaiting the agreed follow-up", nothing less friendly than that. |
|
Oh my bad! I remediated this last week right after your comment and committed it locally, got sidetracked and forgot to push it. Will do a quick re-review and get it updated! |
Signed-off-by: Dustin Persek <dustin.persek@protonmail.com>
Signed-off-by: Dustin Persek <dustin.persek@protonmail.com>
Signed-off-by: Dustin Persek <dustin.persek@protonmail.com>
Signed-off-by: Dustin Persek <dustin.persek@protonmail.com>
0850901 to
be9faf9
Compare
|
Updated this draft with the ground-up current-main follow-up we agreed on: fixed 250-candidate pagination, deterministic score/id ordering, explicit truncation metadata, and no score threshold. The branch was independently reviewed and the focused MCP suite is green. Ready for your re-review when convenient. |
|
Thank you for this one, and for sticking with it across three rounds — the rework you pushed on 08-28 does exactly what we agreed on 08-20, and it passes its suite (I built I owe you an honest account of why it is being closed anyway. On 08-13, a week before I asked you for the fixed 250-result window, a lossless design for the same problem was already being built on our lean-output branch (#1597): a certified top-K prefix with dedicated Four things in your diff are still better than what main has, and they are being landed on main's contract with you as co-author:
I'll link the PR that carries them here. If you would rather open that one yourself, say so and it is yours — the offer stands either way, and the original fix for #915 already went out in |
|
The four residuals are up as #2067 with you as co-author: non-string keyword elements are rejected, a store error during the vector scan now fails closed as "semantic search failed" (with a sqlite_master probe so a not-yet-built vector index still reads as empty + hint, as main intended), the keyword/node norms are hoisted out of the pair loop, and the strdup results are checked. It merges on green. Thank you again. |
Land the four residual hardening points from DeusData#938 on the current semantic-search design (semantic_limit/offset, the certified top-K fetch loop): - extract_semantic_keywords rejects a semantic_query array that holds a non-string element instead of silently running the string subset; the caller gets the same isError as for a bare-string semantic_query. - run_semantic_query_core fails closed when the vector scan itself fails ("semantic search failed", isError) instead of rendering a store error as zero matches plus the broaden-your-keywords hint. cbm_store_vector_search now tells the two states apart: a store with no node_vectors table (lean index) is CBM_STORE_NOT_FOUND and keeps the empty-page + moderate/full-index hint that main relied on by swallowing the "no such table" prepare failure; a failing scan is CBM_STORE_ERR. - vs_min_cosine_score hoists the keyword norms (once per search) and the node norm (once per node) out of the per-(node, keyword) loop. The integer sums are exact, so every score stays bit-identical. - vs_append_result checks its strdup results, frees the partial row on failure and counts a row only once every copy succeeded, so the caller's free path stays valid. Two reproduce-first tests in the mcp suite fail on main and pass with the change: non-string elements produce the type error, and a forced cbm_cosine_i8 failure produces the fail-closed error for semantic-only and combined calls while a missing node_vectors table still yields the lean-index hint. Refs DeusData#915 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com> Co-authored-by: Dustin Persek <dustin.persek@protonmail.com>
What does this PR do?
Adds the focused follow-up agreed in #938 for semantic-query pagination and deterministic ordering:
offset/limittotal,has_more, andtruncatedmetadata in tree and JSON outputThe bounded window is explicit:
truncated=truemeans 250 candidates filled the window, not that the response represents the full corpus.Fixes #915.
Checklist
git commit -s) — required, CI rejectsunsigned commits (DCO, see CONTRIBUTING.md)
make -f Makefile.cbm test)make -f Makefile.cbm lint-ci)Local focused proof passes (
209 passed, 6 skipped). The full local suite is baseline-limited by five CLI assertions that reproduce unchanged on pristinemain; two later daemon timing failures passed an immediate focused 57-test rerun.