Skip to content

fix(cursor): keep the line range when bridging the native read tool - #15036

Merged
diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.51from
QuangBlue:fix/cursor-read-range
Sep 29, 2026
Merged

diegosouzapw merged 6 commits into
diegosouzapw:release/v3.8.51from
QuangBlue:fix/cursor-read-range

Conversation

@QuangBlue

Copy link
Copy Markdown
Contributor

Problem

Cursor models read files with their native read_args exec, which the executor bridges to the client's declared read tool. The decoder kept only ReadArgs.path and dropped offset (field 4, int32) and limit (field 5, uint32), 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 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-routed Read calls 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=16 was forwarded to the client as Read {"file_path": "…/service.py"}.

Fix

  • cursorAgentProtobuf: decode offset (sign-extended int32) and limit (uint32) on exec_read.
  • builtinToolBridge.readBridge: forward them when the client's read tool declares offset/limit as integer/number properties 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

  • Decoder: offset/limit kept; negative offset decoded as int32.
  • Bridge: Claude Code's Read schema (verbatim, integer + bounds), a number-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.ts passes (the sigkillFollowupMs timing test is flaky under load and passes on its own). check-open-sse-typecheck is clean.

⚠️ base-red inherited: #15032 (check-file-size flags tests/unit/response-sanitizer.test.ts on the base tip; this PR does not touch it).

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.
@diegosouzapw
diegosouzapw merged commit 113de57 into diegosouzapw:release/v3.8.51 Sep 29, 2026
4 of 7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants