read_code says an empty body "PROVES the range does not exist" — but it proves it about the SERVER's tree, which is not the caller's when a worktree is in play #258
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#258
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?
Found by a worktree-isolated agent working on this repository — which is this project's own primary working pattern, so the blast radius is us.
The claim
crates/mcp-server/src/server.rs:16743:PROVES is unqualified. It is a strong word chosen deliberately — the surrounding comment shows the note was written precisely to resolve an ambiguity, and v0.19.3's version of it was found stating that ambiguity backwards.
Why it is false in a context the tool cannot see
The bytes are read live from disk at request time — but from the server's project root. An agent working in a git worktree (
.claude/worktrees/agent-*) asks for a relative path and gets an answer about the primary checkout's copy of that file, not its own.Measured, by the agent that hit it: a
read_codeof a range that exists in its file returnedrequested_range_past_end_of_file. Its own edits were invisible; the file it was shown was a different file with the same relative path. The note told it the range had been proven not to exist.The same applies to
search_textand every symbol lookup — an agent cannot dogfood the tools on its own edits — but onlyread_codephrases its answer as a proof.The information already exists, one field away
Every reply carries it:
The server KNOWS there are sibling worktrees and which head it served. The provenance block and the proof-claim simply are not connected — which is this repo's own recurring shape: the disclosure is built, and then not attached to the sentence that needs it.
Shape of a fix
Not "delete the word PROVES" — the strong claim is correct and useful in the ordinary single-tree case, and weakening it everywhere would cost the reader the thing the note was written to give them.
Attach the scope instead. When
sibling_worktrees > 0, the proof is about a named tree and must say so — the head it served is already in hand, so the note can carry it:That is one clause, gated on a fact the reply already computes, and it leaves the single-tree wording unchanged.
What must be graded
sibling_worktrees > 0gets the scoped wording, one with0gets the unscoped wording — both directions, since a note that is always scoped is as wrong as one that never is;Related
Same family as #243 (a disclosure may not name a remedy that cannot answer it) and #254 (a discriminator built and then dropped before the record). The recurring lesson is that the disclosure exists and is not wired to the claim it qualifies.
Fixed. And I did not implement the design proposed above — the simpler one is better, so here is the reasoning rather than a silent substitution.
What the proposal asked for, and why I dropped it
The issue proposed gating the scoped wording on
sibling_worktrees > 0, so a single-tree deployment keeps the unqualified claim. That needs the sibling count at the note site. It is reachable —CodeIndexServer::tree_statescaches aRootProvenanceper root behind a TTL — but it arrives as three states, not two: measured-zero, measured-nonzero, and unmeasured-or-not-yet-cached. Getting that right means the unqualified proof may only be emitted on a MEASURED zero, since "absent is not zero" is this repo's rule. That is new state to keep honest, two fixtures to maintain, and a cache read on a path that exists only when a read returns nothing.The gain it buys is that single-tree users are spared one clause. Weighed against it: naming which tree answered is useful to every caller, not a caveat — it tells them where to look next — and it is unconditionally TRUE, so there is no state to get wrong.
What shipped
PROVESbecameproves … IN THE TREE THIS SERVER INDEXES. The claim keeps its force and gains its scope.Cost is on a path that only exists when nothing was served, so it is not the
sibling_worktreessituation the provenance module documents — always-emitting"sibling_worktrees":0was measured at 578 tokens, 5.2% on theplugin-wpfbench and tripped the 1.05x ceiling. This note is emitted only on an empty multi-line read; normal replies are unchanged, and the bench is green.Graded, with the mutation run
mcp_smoke::read_code_past_eof_says_so_instead_of_inverting_the_rangealready asserted the note's PREFIX, which could not see the scope. It now also asserts the note containsindexed_trees— the field name, not the prose, because the reader's next action is to look at that field and a field name is stable where a turn of phrase is not.MUTATION (RUN): restore the unqualified
PROVES the range does not exist —.RESULT: RED, md5
a9d37d69…→26d35fb3…, with the failure printing the offending note in full. Restored; md5 back toa9d37d69….One detail the mutation output surfaced: in that fixture the provenance reads
indexed_trees: {"primary": {"unavailable": "not_a_git_checkout"}}. So the pointer stays informative even where the tree is not a checkout at all — it says so, rather than leaving the reader to assume.What this does not fix
search_textandsearch_symbolshave the same underlying scope — a worktree-isolated caller gets answers about the primary checkout — but neither phrases its answer as a proof, which is what made this one actively misleading rather than merely narrow. That the tools cannot be dogfooded on a lane's own edits is a real cost of the worktree workflow and remains open; it is a routing question, not a wording one.