fix(search): surface daemon tracebacks and reject NULL KNN distances - #271
Merged
Conversation
…nces Issue #270 reported `ccc search --path` crashing with `TypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'`. The crash could not be reproduced, and the reported root cause does not hold: `vec_distance_L2` never returns NULL, it raises (verified against sqlite-vec 0.1.6-0.1.9, SQLite 3.46/3.53, multi-chunk tables and re-index churn). What the report did expose is that the failure was undiagnosable. Two gaps, both fixed here: - The daemon's search handler discarded the traceback (`ErrorResponse(message=str(e))`), so the reporter saw only the client's re-raise frames. It now sends `traceback.format_exc()` and logs the exception; `_dispatch` does the same. On the client, the `raise RuntimeError(f"Daemon error: ...")` pattern was duplicated at five sites and only `doctor()` appended `resp.traceback` -- the search path, which the reporter came through, dropped it. Consolidated into one `_daemon_error()` helper used by all five. - `_knn_query` now rejects rows with a NULL distance, raising a specific error naming the query shape and offending file instead of dying in `_l2_to_score`. `distance` is a hidden vec0 column that sqlite-vec populates only under the KNN query plan and returns NULL for on a full scan, so a NULL means the plan we asked for is not the plan we got. The guard lives in `_knn_query` rather than the caller because the multi-language merge path sorts on `r[5]` in `heapq.nsmallest`, where a NULL would fail on `None < float` before any caller-side check ran. `_full_scan_query` needs no guard: it computes `vec_distance_L2(...)` itself, which works under any plan and raises rather than returning NULL on bad input. The new tests build a real in-memory vec0 table with the indexer's exact DDL (no embedding model, ~0.2s) and cover both that `--path` filtering yields usable distances and that the bare-`distance`-under-full-scan shape is the one that does not. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
_search_with_waitand_dispatchpopulateErrorResponse.traceback(and log the exception), and the client's five duplicatedDaemon error:raise sites are consolidated into one_daemon_error()helper that appends the traceback everywhere, not just indoctor(). Motivated by ccc search --path crashes the daemon: unsupported operand type(s) for *: 'NoneType' and 'NoneType' #270, which arrived with no frame pointing at the failing code._knn_queryrejects rows with a NULLdistancewith a specific, reportable error instead of letting the NULL reach_l2_to_scoreand die asTypeError: unsupported operand type(s) for *: 'NoneType' and 'NoneType'.distanceis a hidden vec0 column populated only under the KNN query plan (NULL on a full scan), so a NULL means the plan we asked for is not the plan we got._full_scan_queryneeds no guard — it computesvec_distance_L2(...)itself, which works under any plan and raises rather than returning NULL.tests/test_query_filters.pyruns against a real in-memory vec0 table using the indexer's exact DDL (no embedding model, ~0.2s):--pathand--lang+--pathfull scans yield usable distances (the claim in ccc search --path crashes the daemon: unsupported operand type(s) for *: 'NoneType' and 'NoneType' #270 — not reproducible), and the bare-distance-under-full-scan shape is pinned as the one that yields NULLs.Note: this does not close #270 — the crash could not be reproduced (see the issue thread for the analysis); this makes any recurrence self-diagnosing.
Test plan
CI, plus new tests:
tests/test_query_filters.py(vec0 SQL layer),test_daemon_error_carries_daemon_side_traceback(client),test_search_failure_reports_daemon_side_traceback(daemon).🤖 Generated with Claude Code