fix(cursor): keep the line range when bridging the native read tool - #15036
Merged
diegosouzapw merged 6 commits intoSep 29, 2026
Merged
Conversation
Cursor's ReadArgs carries an optional offset/limit. The decoder kept only the path, so every partial read reached the client as a whole-file Read. Claude Code answers a repeated whole-file read of an unchanged file with "Wasted call — file unchanged", so the model never got the lines it asked for and retried the same read indefinitely (up to 342 times in one prod session). Decode offset (int32) and limit (uint32), forward them when the client's read tool declares integer offset/limit properties that accept the values, and fail closed (typed rejection) when the tool cannot carry the range.
# Conflicts: # config/quality/file-size-baseline.json
diegosouzapw
merged commit Sep 29, 2026
113de57
into
diegosouzapw:release/v3.8.51
4 of 7 checks passed
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.
Problem
Cursor models read files with their native
read_argsexec, which the executor bridges to the client's declared read tool. The decoder kept onlyReadArgs.pathand droppedoffset(field 4, int32) andlimit(field 5, uint32), so every partial read reached the client as a whole-file read.Claude Code answers a repeated whole-file
Readof an unchanged file with "Wasted call — file unchanged since your last Read". The model never got the lines it asked for and retried the same read indefinitely. On our deployment, 1877 of 2306 Cursor-routedReadcalls over three days got that answer, and one session read the same file 342 times. It is worst after Claude Code's own auto-compaction, when the file content is gone from context but Claude Code still considers it read, so a ranged read is the model's only way back to it.Wire capture of the failing turn:
exec_read path=…/service.py offset=1195 limit=16was forwarded to the client asRead {"file_path": "…/service.py"}.Fix
cursorAgentProtobuf: decodeoffset(sign-extended int32) andlimit(uint32) onexec_read.builtinToolBridge.readBridge: forward them when the client's read tool declaresoffset/limitasinteger/numberproperties whose bounds (minimum,maximum,exclusiveMinimum,exclusiveMaximum) accept the values. A read tool that cannot carry the range is not a match, so a ranged read keeps the typed rejection instead of silently becoming a whole-file read.Tests
Readschema (verbatim,integer+ bounds), anumber-typed read tool, a path-only tool (not bridged), out-of-bounds values (not bridged), unranged read unchanged.Reproduced and verified against a real Cursor model through a local router with Claude Code (
claude -p): before, 2/3 runs looped to the turn limit with 9–11 "Wasted call" results; after, 3/3 answered correctly with none.node --import tsx/esm --test tests/unit/*cursor*.test.tspasses (thesigkillFollowupMstiming test is flaky under load and passes on its own).check-open-sse-typecheckis clean.check-file-sizeflagstests/unit/response-sanitizer.test.tson the base tip; this PR does not touch it).