The context tax: why your coding agent reads the same 600 lines 400 times

10 min read

When a coding agent works in a codebase that doesn't fit in its context window, it navigates the only way a shell can: grep, then read a file, then read a bigger slice of the file. 

  • Every one of those reads stays in the conversation and is re-billed on every later turn
  • On one ordinary ~800-line pull request in our own codebase, that added up to 156 million context tokens and a context window that peaked at 459k tokens, for a change whose final diff a person could read in five minutes.

Sonar Vortex is our enterprise harness for AI coding agents, guiding them with the right context and verifying every change in real time. To provide guidance, it works inside the agent's loop and answers navigation questions from a graph of your codebase instead of from raw file reads. That graph is built by SemSitter™, our in-house semantic navigation engine, which keeps a local Unified Dependency Graph (UDG) of the repository updated instantly on every change.

  • Instead of grep, then a slice, then a wider slice, the agent queries the graph for a specific node and gets back that node plus its typed relationships. 
  • The measurable effects are less context carried per task, fewer round-trips, and, in a repository larger than the window, call sites a regex would not have matched.

The story of a refactor that shouldn't have hurt

We build SemSitter with coding agents: that makes it a great candidate to show how agentic navigation capabilities can make the difference. A recent PR taught SemSitter's Python analyzer to do call-site resolution, that is, for a method call, record which type owns the method and what the call returns. The C# analyzer already did this, so the job was to mirror it in Python.

Well into the change, the agent had to work with this line of SemSitter's own code:

let return_type = ctx.method_index.resolve_return_type(owner, method_name);

To touch it safely, the agent needed the answer to the most ordinary question in programming, the one your IDE's "Go to Definition" gives you for free: for the call ctx.method_index.resolve_return_type(...), what type is ctx.method_index, where is resolve_return_type defined, and what does it return? The honest answers: ctx.method_index is a MethodIndex, the method lives in method_index.rs, and it returns Option<&str>.

The agent had no index of its own. So it did what a shell allows:

  1. grep -rn "resolve_return_type", and got back a definition in every backend at once: Python, TypeScript, Java, Rust, C#, and a shared core. The regex cannot say which one this call binds to.
  2. Open method_index.rs to read the definition. It doesn't know where the function starts or ends, so it reads a generous slice, or the whole file.
  3. The return type is produced by a helper, extract_type_name. Grep for that, open that file, read another wide slice.
  4. Is ctx.method_index really a MethodIndex? Grep for the field, open the struct, repeat.

Each of those reads is now permanently in the conversation, and you are going to pay for it, over and over again, until the conversation is over.

There is a small irony here worth stating plainly: the agent was building the exact capability it was missing. Call-site resolution is "given this call, hand me its owner type and return type." The agent needed precisely that to navigate the code, and, lacking it, fell back to grep and whole-file reads.

What it actually cost us (with real numbers)

While building SemSitter with coding agents, we keep the full traces. So we can measure the tax precisely. 

Here's one pull request from the SemSitter repo, a backend change of about 800 lines that added call-site resolution (more on that below):

Metric (one PR, measured)

Value

Model round-trips in the session

512

Context window at its peak

458,700 tokens

Fresh input tokens

106k

Cache-read tokens (the re-billed transcript)

152.8 million

Cache-write tokens

3.1 million

Output tokens

289k

Total context tokens billed

≈ 156 million

Approx. cost of the session

≈ $41

The line that matters is cache-read: 152.8 million tokens. Here's why it's so large.

The mechanism: context is a tax you pay every turn

A coding agent doesn't read a file once. On every new step, the model is re-sent the entire conversation so far as input. Prompt caching makes those repeated tokens cheap per unit (≈10% of the input price), but you still pay for them on every single turn. So the true cost of a token isn't its size. It's its size times the number of turns it survives.

Read a 600-line file on turn 40 of a 512-turn session and you haven't paid for 600 lines. You've paid for 600 lines × ~470 more turns.

Context size per model round-trip, measured from the real session. It only grows: every file read and tool result stays in, and is re-sent, until the run ends or the window is compacted.

One over-read, traced end to end

Early in that PR, the agent needed to understand one helper: a ~67-line function that, given a value, works out its type name. To find it, the agent read the whole 618-line file (6,472 tokens) instead of the 67 lines it used (~700 tokens).

  • Wasted immediately: ~5,770 tokens.
  • That read entered the conversation around turn 42 and stayed for the remaining 470 turns.
  • Re-billed as a cache read each turn: 5,770 × 470 ≈ 2.7 million tokens of pure waste.
  • At current cache-read pricing (~$0.20 / million), that's ≈ $0.54, for one unnecessary file read.

Fifty-four cents sounds trivial. But this PR did that ~10 times, plus dozens of blind tree-wide greps, several of which returned nothing and forced a second, wider grep. Add it up and the avoidable navigation overhead alone runs into several dollars on a single $41 PR, and it scales with the size of the repo, not the size of your change.

And this is not one unlucky PR

Across 18 comparable single-ticket PRs in the same repo, the average was:

  • ~234 million context tokens per PR
  • ~$65 per PR (median ~$52)
  • ~700 model round-trips per PR
  • context windows routinely peaking between 450k and 975k tokens, brushing the 1M ceiling, at which point the agent is forced to compact and lose earlier context entirely.

Context tokens billed to complete each of 18 comparable PRs in the same repository. The spread is wide because it tracks how much of the codebase the agent had to traverse, not how large the final diff was.

That last point is the second, quieter failure mode. In a repo too big for the window, grep doesn't just cost tokens. It misses. A regex finds the strings you thought to search for, not the call that reaches your function through an interface, an alias, or another programming language. Missed call sites become failed builds, another round-trip to CI, and more rework, each with its own fresh context tax.

What SemSitter in Sonar Vortex does instead

This is what Sonar Vortex does instead, using SemSitter, the navigation engine underneath it. SemSitter builds a Unified Dependency Graph (UDG) of the codebase: every function, method, class, field and parameter is a node, and the relationships between them are typed edges: calls, references, returns, has-param, is-type, contains, extends. How we build that graph accurately, across languages, leveraging parallelism and incrementality while scaling to millions of nodes, is the interesting part we keep under the hood.

The point for the agent is simple. It no longer asks the filesystem "which files mention resolve_return_type?" It asks the graph a precise, semantic question:

"Give me the definition of resolve_return_type that this call binds to, the type that owns it, its return type, and its callers."

and gets back exactly that: the one method body, plus the edges that answer the rest, with no surrounding file, no six-way grep, and nothing to widen.

That is, in fact, the feature the PR was building. For the call ctx.method_index.resolve_return_type(...), the graph records which type owns the method (a references edge from emit_call to MethodIndex, because ctx.method_index is a MethodIndex) and what it returns (resolve_return_type carries a returns edge to Option<&str>). The agent gets "where is this defined, on what type, and what comes back?" as data, not as a page of text it has to read and then carry forever.

The graph the agent navigates

The subgraph SemSitter returns for the call ctx.method_index.resolve_return_type(...). Blue nodes are code, green is documentation, orange is code in another language.

When the agent touches resolve_return_type, the graph hands it the small, exact neighborhood shown above. Compare the two ways of answering the same three questions:

Question

grep + read

SemSitter (graph)

Which resolve_return_type does this call bind to?

grep returns one per backend; open files to guess

one references edge → the Python MethodIndex

What does it return?

read the file until the signature is in view

one returns edge → Option<&str>

Who calls it, and on what type?

grep again, per file, hope the regex matched

upstream calls + owner references edges, complete

Tokens dragged through the rest of the chat

thousands, per file, × every later turn

the one method body, once

Same answers. One is a page of text you re-pay for on every turn and that still misses the indirect callers. The other is a handful of typed edges that are complete by construction.

The single over-read from earlier, framed as a lookup. Loading ~9x fewer tokens matters most because of the multiplier: this read survived 470 turns, so grep+read carried ~2.7M extra cache-read tokens versus ~0.33M for the graph lookup.

Why the gains compound: beyond code-to-code

Structural edges (calls, returns, references) are only the first layer. The same graph carries two more kinds of relationship, and each multiplies the savings on exactly the tasks that hurt most today.

  • Code → text (documented_by). Every code node can be linked to the specific piece of documentation that governs it: the one paragraph of a design doc, the one ADR, the one section of the README. This is the "misleading docs" problem turned on its head. Instead of the agent scraping a wiki and being led astray by a stale page, touching resolve_return_type surfaces the note on how MethodIndex is built from the callees' -> annotations: a few hundred exact tokens instead of a document dump, and the right few hundred.
  • Text → text. Documentation, tickets and design notes are linked to each other by meaning, so the agent can follow "this rule is refined by that ADR" without a full-text search that returns fifty near-misses.
  • Code → code, across programming languages. The same idea implemented in two languages can be linked even when the names differ. This PR was mirroring C#'s call-site resolution in Python, and C#'s return-type logic lives in a function called resolve_type_node, not resolve_return_type. A grep for one name will never surface the other. A cross-language rename or contract change, today one of the most token-expensive, error-prone things you can ask an agent to do, because it means grepping several backends in several syntaxes, becomes follow the edge. Both ends arrive in context together, and neither is missed because the other language happened to use a different word.

In the diagram above, those are the green documented_by edge and the dashed semantically_related edge to the C# backend. Each one is a navigation the shell simply cannot make: grep cannot cross from code to prose, and it cannot connect resolve_return_type in the Python backend to resolve_type_node in the C# one. The graph can, and it hands the agent only the node it asked for.

The takeaway

The bottleneck for AI coding agents in real, large codebases isn't reasoning. It's navigation, and navigation-by-grep has two costs that don't show up until you measure:

  1. Token cost. Every blind read is re-billed on every later turn. On one ordinary PR that was 156M context tokens and ~$41; across a batch it averaged ~$65 a PR, with context windows brushing the 1M ceiling.
  2. Correctness cost. grep finds strings, not meaning. What it misses becomes rework and extra trips to CI, each paying the token tax again.

SemSitter, our in-house code navigation engine powering Sonar Vortex, turns navigation from a text search into a graph query. The agent carries the nodes it asked for rather than the files it had to scan, and it can follow relationships across files, docs, and languages that a regex cannot express. 

The result is lower context cost per change and fewer call sites missed, and both effects grow with the size of the codebase.

If your agents work in a codebase bigger than their context window, this tax is already on your bill. Sonar Vortex is how you stop paying it.

Build trust into every line of code