search_text: pinned whole-word pagination omits the generation stamp and accepts stale cursors #225
Labels
No labels
code-review
correctness
dos
performance
security
severity/high
severity/low
severity/medium
tech-debt
Kind/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
h-dv/code-index#225
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The pinned
search_text(whole_word=true)branch mints bare offsets even when the active generation is known. Consequently, it bypasses the stale-generation rejection added in #127.Reproduction
Against this repository, compare
search_textcalls:The ordinary call returns
next_cursor: "1@fbe68000"; the whole-word call returnsnext_cursor: "1". Both have more results and both know the same generation.In a disposable 0.27.0 index with three files containing
needle, obtain both cursors, change the active generation identity, and replay them:invalid_cursor;"2".The reproduction used controlled SQLite fault injection only in the disposable database:
This isolates cursor validation while leaving searchable text constant. It is not a test of the package activation workflow or evidence that promotion can happen mid-request. The practical stale-cursor window includes a daemon restart after promotion/rollback and snapshot-mode clients, as discussed in #127.
Cause and suggested fix
The whole-word branch constructs
Some(next_off.to_string())directly. The ordinary branch uses the shared epoch-awarenext_cursorhelper.parse_cursorintentionally accepts unstamped cursors for compatibility.Replace the hand-built branch with:
This retains count-only behavior and saturating arithmetic while stamping newly minted cursors whenever the generation is available. Keep acceptance of legacy unstamped cursors; the defect is minting a new unstamped cursor despite knowing the generation.
Acceptance tests
invalid_cursor.limit=0still returns no cursor; end-of-results and overflow handling remain correct.next_off.to_string()mint must fail the regression.Validation
Confirmed on a freshly built 0.27.0 (
a32a7519be), using MCP over stdio in disposable projects. Also dogfooded the connected 0.26.1 server against this repository. The reviewed Rust implementation is unchanged between those commits. Existing checks pass: 241 MCP binary unit tests and 8 daemon RPC integration tests; these cases are not covered by those passing tests.Priority assessment: P2 / medium correctness. A changed result ordering can otherwise cause silent omissions or repeats. Related: #127 (closed; this is the remaining whole-word branch).
Shared evidence for #224, #225 and #226:
Extract the ZIP, build the reviewed checkout with
cargo build --offline -p code-index-mcp -p code-index-cli, then runpython3 /path/to/repro.pyfrom that checkout. The script uses disposable fixture projects and an isolated plugin store; the generation change is fault injection only in a disposable database.Fixed in
492c0d3, onmaster, shipping in v0.27.0.Whole-word pagination minted cursors without the generation stamp, so they bypassed the stale-generation rejection that every other cursor path is subject to. Whole-word cursors are now stamped and validated like the rest.
Closing on merge.