read_code rejects the integer symbol id that search_symbols just handed it, breaking the one documented zero-hop read #121
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#121
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?
Dogfood finding from #51, whose lane built the agent-task benchmark using our own tools throughout.
The defect
search_symbolsreturns rows whoseidis a JSON integer. Every id-taking tool accepts it —find_callers,find_callees,find_references,change_impact,get_symbol.read_codedoes not. It rejects the integer withinvalid_argument_type.And
read_code's own description advertises exactly this call as the efficient path:So the single documented no-lookup-hop path is the one call an agent cannot make by pasting the field it was just given. The workaround is to stringify, which an agent has no reason to guess and which the description does not mention.
Why this is worse than an ergonomic wart
The whole value proposition measured in #51 is round trips: 62 against ripgrep's 247. The zero-hop read is one of the mechanisms that buys that, and it is the mechanism most likely to be abandoned after one
invalid_argument_type— an agent that hits a type error on the documented path will fall back topath:line-line, spending the lookup hop the design exists to avoid.It also violates a rule this project applies elsewhere without exception: a field a tool emits must be usable as the field another tool takes. The handle work (#31/#32,
cih2_stable handles) exists precisely so identity round-trips across calls; this is the same contract broken in the simplest possible place.The fix
Accept both shapes in
read_code'starget: the integer id as emitted, and the string form. The tolerant direction is the only safe one — an id that arrives as a number is a correct use of the documented API, and refusing it is the bug.Then a gate in the shape this repo already uses for wire contracts: for every tool that takes a symbol id, the value
search_symbolsemits must be accepted verbatim. A test that checksread_codealone would go stale the first time a new id-taking tool is added — the same staleness that made #107's registry find six ungraded URIs where the filing named four.Mutation: revert the tolerant parse; the registry test must go red naming
read_code, not merely one e2e.Related
Found alongside #118 (a
#[tool]method gets a falsely earned zero fromsearch_symbols) — both are defects in the seam between what a tool emits and what the next tool can consume.Fixed — and the registry found three tools, not the one this issue named.
read_code.target,get_symbol.idandget_dependencies.targetare all string-declared union arguments ("<path>… or<symbol_id>", "a numeric symbol id … orpath:line", "Path or symbol_id"). Every one of them rejected the integersearch_symbolsemits. The filing named onlyread_code— exactly the #107 pattern, where a registry over the ungraded URIs came back with six where the issue listed four.The fix is a schema widening, not a special case
A
SymbolIdOrStringmarker publishes"type": ["string","integer"], plusde_symbol_id_or_string, which normalises the integer to the decimal string the handlers already parse withstr::parse::<i64>(). Widening the declaration teaches the pre-dispatch argcheck ladder, the published schema and serde in one move; downstream behaviour is unchanged.One detail worth recording because it is the kind of thing that quietly disables a gate: the union type is inlined, never a
$ref. A$refmakesargcheck::check_typesstep aside and stop grading that argument altogether — so the "tolerant" form would have been tolerant of everything, including genuinely wrong types.The gate
crates/mcp-server/tests/symbol_id_args_registry.rs— every published tool must appear with an explicit classification of its symbol-id arguments, checked in both directions againsttools/list. Six gates:SYMBOL_ID_ARGSinserver.rs;id/*_id/*_idscannot hide behindid_args: &[].Where both spellings are published, the two calls must return byte-identical bodies.
Gate 6 is the one that makes this durable: without it, a future tool could add a symbol-id argument and declare itself as having none, which is precisely how a registry goes stale while staying green.
Mutations, run
Revert the tolerant parse on
ReadCodeArgs::target:Delete the
read_coderow from the registry:these published tools have no row in REGISTRY … ["read_code"].Payload
The three type widenings cost ~9 tokens, and the same three descriptions were trimmed to more than pay for it —
get_symbol.id's "(string-encoded)" had become actively wrong. Startup payload is 16,223, five tokens BELOW the 16,228 it started at, and 332 under the ceiling.project_overviewuntouched.change_impactreturns an empty, confident answer wherefind_callersfinds 5 call sites — and nothing in its payload says why #122