<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Blog · Felipe Basurto</title>
    <link>https://felipebasurto.com/blog/</link>
    <atom:link href="https://felipebasurto.com/blog/feed.xml" rel="self" type="application/rss+xml" />
    <description>Notes on building with AI, shipping software, and things I find worth writing down.</description>
    <language>en</language>
    <dc:creator>Felipe Basurto</dc:creator>
    <lastBuildDate>Wed, 23 Sep 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>Model context is not static</title>
      <link>https://felipebasurto.com/blog/model-context-is-not-static/</link>
      <guid isPermaLink="true">https://felipebasurto.com/blog/model-context-is-not-static/</guid>
      <pubDate>Wed, 23 Sep 2026 00:00:00 GMT</pubDate>
      <author>hello@felipebasurto.com (Felipe Basurto)</author>
      <dc:creator>Felipe Basurto</dc:creator>
      <description>In 78% of 67,074 public coding-agent runs, the agent kept re-sending code that its own edit had made stale. I built a context engine, based on the CORVUS paper, that keeps that code current. It works byte for byte, but the model did not reliably do better, and requests did not get cheaper.</description>
      <content:encoded><![CDATA[<p class="md-p">A coding agent reads a function:</p>
<div class="md-codeblock"><div class="md-codeblock-gutter" aria-hidden="true">```js</div><pre class="md-pre"><code class="md-code language-js"><span class="hljs-keyword">function</span> <span class="hljs-title function_">total</span>(<span class="hljs-params">quantity</span>) {
  <span class="hljs-keyword">return</span> quantity * <span class="hljs-number">10</span>;
}</code></pre></div>
<p class="md-p">Then something or someone edits the file: the agent itself, a formatter, another process or another agent. Now the multiplier is <code class="md-codespan"><span class="md-muted">`</span>20<span class="md-muted">`</span></code>. What does the model see on its next request?</p>
<p class="md-p">The file on disk says <code class="md-codespan"><span class="md-muted">`</span>20<span class="md-muted">`</span></code>. The model still sees <code class="md-codespan"><span class="md-muted">`</span>10<span class="md-muted">`</span></code>, because the agent&#39;s earlier read is still in the model&#39;s context, and most agents re-send the whole conversation on every request. The tools work on one version of the file while the model reasons from another.</p>
<p class="md-p">What usually happens is that the agent can read the file again, and it often does, but then it depends on the model deciding to. I built <a class="md-link" href="https://github.com/felipebasurto/freshctx" title="https://github.com/felipebasurto/freshctx" rel="noopener noreferrer" target="_blank">FreshCtx</a> to take that job away from the model. It is a local context engine: a program that rebuilds a current view of the code the agent has read before every request is sent.</p>
<p class="md-p">This post measures how often the problem shows up in public agent runs, explains the paper FreshCtx is based on and how the engine works, and then answers three questions:</p>
<ul class="md-list md-list--unordered">
<li class="md-li">Is the code the model receives current?</li>
<li class="md-li">Does the model do better with it?</li>
<li class="md-li">What does it cost?</li>
</ul>
<details class="md-details">
<summary class="md-details__summary">Words used in this article</summary>

<ul class="md-list md-list--unordered">
<li class="md-li"><strong class="md-strong">Harness:</strong> the program around the model that runs tools, records their results and builds each model request.</li>
<li class="md-li"><strong class="md-strong">Request:</strong> everything sent to the model in one call, usually the whole conversation so far.</li>
<li class="md-li"><strong class="md-strong">File view:</strong> a tool result that shows a file or some of its lines.</li>
<li class="md-li"><strong class="md-strong">Stale:</strong> the file has changed since the view, so at least one line it shows no longer matches the disk.</li>
<li class="md-li"><strong class="md-strong">Marker:</strong> a short placeholder that replaces an old file view in the request.</li>
<li class="md-li"><strong class="md-strong">Projection:</strong> the current code for what the agent has read, added once at the end of the request.</li>
<li class="md-li"><strong class="md-strong">Prompt cache:</strong> providers charge less for the start of a request when it is identical to the start of an earlier one.</li>
</ul>
</details>

<h2 class="md-heading" id="every-coding-agent-sees-old-code"><span class="md-hashes" aria-hidden="true">##</span> Every coding agent sees old code</h2>
<p class="md-p">Cursor, Claude Code, Codex, Devin, OpenCode, OpenHands, Hermes and Pi all run the same basic loop. The model reads the request and decides what to do next. The harness gives it tools, such as reading a file, editing one or running a command: the model asks for one, the harness runs it and adds its output, the tool result, to the conversation. The model never touches the disk. Everything it knows about the code comes from tool results in the request.</p>
<p class="md-p">The simplest way to build the next request, and the usual one, is to re-send the whole conversation so far, with every tool result in it, until compaction trims it:</p>
<p class="md-p">A file read does two jobs: it records what a tool returned at one moment, and it supplies source code for later reasoning. The two agree until the file changes. Keeping the old result is useful for debugging; reusing it as current source is the risk. The conversation is a cache that never refreshes.</p>
<h2 class="md-heading" id="how-often-it-happens"><span class="md-hashes" aria-hidden="true">##</span> How often it happens</h2>
<p class="md-p">To count it, I used a public dataset, <a class="md-link" href="https://huggingface.co/datasets/nebius/SWE-rebench-openhands-trajectories" title="https://huggingface.co/datasets/nebius/SWE-rebench-openhands-trajectories" rel="noopener noreferrer" target="_blank">nebius/SWE-rebench-openhands-trajectories</a>. It holds 67,074 runs of OpenHands, an open-source coding agent, using the Qwen3-Coder-480B model to fix real GitHub issues, with every request recorded. Compaction was switched off, so every request carries the whole conversation.</p>
<p class="md-p">The rule was strict: a view counts as stale only when the next thing to touch its file is the agent&#39;s own edit, changing a line the view showed. Reads through shell commands such as <code class="md-codespan"><span class="md-muted">`</span>cat<span class="md-muted">`</span></code> do not count.</p>
<p class="md-p">Here is one interesting case I found. A task asked the agent to fix a bug in <a class="md-link" href="https://github.com/ucfopen/canvasapi" title="https://github.com/ucfopen/canvasapi" rel="noopener noreferrer" target="_blank">canvasapi</a>, a Python library for the Canvas learning platform. The agent read all 231 lines of <code class="md-codespan"><span class="md-muted">`</span>canvasapi/module.py<span class="md-muted">`</span></code>, then edited the function it had just read. The edit added two lines, so everything below line 126 moved down. From then on, every request carried the old read next to a file that no longer matched it:</p>
<p class="md-p">That is just one run. Across all of them, <strong class="md-strong">78% of runs sent the model at least one request with a stale view, and 22% still did in their very last request</strong>. Counted by request instead of by run, about 1 in 7 requests carried one:</p>
<p class="md-p">A stale view is not proof of a wrong answer; if it were, no agent would work. Models often re-read and recover. But it means the prompt holds two versions of the same code, and the model has to work out which is current.</p>
<h2 class="md-heading" id="the-idea-comes-from-corvus"><span class="md-hashes" aria-hidden="true">##</span> The idea comes from CORVUS</h2>
<p class="md-p">Everything here builds on <a class="md-link" href="https://arxiv.org/abs/2607.22711" title="https://arxiv.org/abs/2607.22711" rel="noopener noreferrer" target="_blank">CORVUS</a>, a paper published two months ago by <a class="md-link" href="https://zmw12306.github.io/" title="https://zmw12306.github.io/" rel="noopener noreferrer" target="_blank">Mingwei Zheng</a> of Purdue University, with <a class="md-link" href="https://davidmobrien.github.io/" title="https://davidmobrien.github.io/" rel="noopener noreferrer" target="_blank">David O&#39;Brien</a>, <a class="md-link" href="https://jncsw.github.io/" title="https://jncsw.github.io/" rel="noopener noreferrer" target="_blank">Siwei Cui</a>, <a class="md-link" href="https://pardisp.github.io/" title="https://pardisp.github.io/" rel="noopener noreferrer" target="_blank">Pardis Pashakhanloo</a>, <a class="md-link" href="https://rajdeepmukherjee.com/" title="https://rajdeepmukherjee.com/" rel="noopener noreferrer" target="_blank">Rajdeep Mukherjee</a>, <a class="md-link" href="https://codingsoo.github.io/" title="https://codingsoo.github.io/" rel="noopener noreferrer" target="_blank">Myeongsoo Kim</a> and <a class="md-link" href="https://sachitkuhar.github.io/" title="https://sachitkuhar.github.io/" rel="noopener noreferrer" target="_blank">Sachit Kuhar</a> of AWS AI Labs. </p>
<p class="md-p">They described this problem before I did, and they named its cause: agents keep an append-only history, where a file read is a fixed snapshot that nothing ever updates. Agents then re-read files they already have, and edit from old snapshots, so edits fail and need recovering.</p>
<p class="md-p">Their fix adds one phase to the agent loop. A new <code class="md-codespan"><span class="md-muted">`</span>sync_file<span class="md-muted">`</span></code> tool puts a file on a list and leaves a short marker in the history. Before every step, a context sync reads each listed file from disk and adds its current contents at the end of the prompt. The prompt holds at most one copy of each file, and that copy is current. Simplified from their first two figures:</p>
<p class="md-p">Across 573 tasks and four models, they report 9% to 50% fewer input tokens, up to about half the cost, and pass rates within about 1.4 points of the baseline. </p>
<p class="md-p">They also found that current files work better at the end of the prompt than near the start. Their future work names two open problems: refreshing selected functions instead of whole files, and prompt caching, which gets harder when part of the prompt changes on every step.</p>
<p class="md-p">That gives three broad ways to handle an old file view:</p>
<p class="md-p"><strong class="md-strong">FreshCtx is my attempt at the first item of their future work: refreshing only the functions the agent read, instead of whole files.</strong></p>
<h2 class="md-heading" id="how-freshctx-works"><span class="md-hashes" aria-hidden="true">##</span> How FreshCtx works</h2>
<p class="md-p">FreshCtx follows the CORVUS idea with four differences:</p>
<ul class="md-list md-list--unordered">
<li class="md-li"><strong class="md-strong">No new tool.</strong> The agent reads files the way it always has, and any successful read counts.</li>
<li class="md-li"><strong class="md-strong">Only the outgoing copy changes.</strong> The saved conversation, what the tools really returned, stays as it was for debugging and replay.</li>
<li class="md-li"><strong class="md-strong">Functions, not files.</strong> It refreshes the function, or the range of lines, that the agent read. On five traces from Express, Flask, Go tools and ripgrep, that made the refreshed code 1.7 to 8.6 times smaller than whole files.</li>
<li class="md-li"><strong class="md-strong">An adapter, not an agent.</strong> It runs as a small local process next to the harness, and a thin adapter inside the harness, the bridge, talks to it.</li>
</ul>
<p class="md-p">Before each request, the bridge gets back a rewritten copy: each old view becomes a short marker, and the current code for everything the agent read, the projection, is added once at the end. For the <code class="md-codespan"><span class="md-muted">`</span>total<span class="md-muted">`</span></code> example:</p>
<p class="md-p">The model gets one current version instead of two that disagree, while the saved conversation keeps <code class="md-codespan"><span class="md-muted">`</span>* 10<span class="md-muted">`</span></code> as the record.</p>
<h3 class="md-heading" id="how-does-it-find-the-function-again"><span class="md-hashes" aria-hidden="true">###</span> How does it find the function again?</h3>
<p class="md-p">The agent reads some lines at some position. After an edit, those lines may have moved, and they may have changed. Line numbers are useless for finding them again, since a lot can happen to a file during a coding session, and asking a model would be slow, expensive and unreliable.</p>
<p class="md-p">So FreshCtx reads the file the way a compiler does. It uses <a class="md-link" href="https://tree-sitter.github.io/tree-sitter/" title="https://tree-sitter.github.io/tree-sitter/" rel="noopener noreferrer" target="_blank">Tree-sitter</a>, an open-source parser used by editors such as Neovim and Zed, which turns each file into a tree: the file is the trunk, each function or declaration is a branch, and each line hangs off the branch it belongs to.</p>
<p class="md-p">When the agent reads a line, FreshCtx notes which branch it hangs from. Before the next request, it parses the file again and looks for that branch by name. Lines can move and change, but the branch called <code class="md-codespan"><span class="md-muted">`</span>total<span class="md-muted">`</span></code> is still the branch called <code class="md-codespan"><span class="md-muted">`</span>total<span class="md-muted">`</span></code>.</p>
<p class="md-p">One line read inside a function brings back the whole function because, even though sending less would be cheaper, half a function is very hard to reason about. A top-level line such as <code class="md-codespan"><span class="md-muted">`</span>RATE<span class="md-muted">`</span></code> belongs to no function, so FreshCtx finds it again by the text around it: a best guess.</p>
<p class="md-p">If a file cannot be parsed, FreshCtx falls back to the whole file, and if the same code appears twice and it cannot tell which one was read, it leaves the code out rather than guess. There is room for improvement here, but it will never be an exact science.</p>
<p class="md-p">Parsing is local and fast enough to run before every request, and Tree-sitter still builds a tree when the code has a syntax error, which happens often while an agent is mid-edit.</p>
<h3 class="md-heading" id="what-is-the-projection-and-why-does-it-have-a-size-limit"><span class="md-hashes" aria-hidden="true">###</span> What is the projection, and why does it have a size limit?</h3>
<p class="md-p">The projection is the block of current code that FreshCtx adds at the end of each request: one copy of every function the agent has read, as it is on disk right now. It is re-sent in full on every request, and it sits in the part of the prompt that is billed at full price, as the cost section shows.</p>
<p class="md-p">Without a limit, every function the agent ever read would come back on every request, so a long session would grow more expensive with each step, and the current code would crowd out the conversation in the model&#39;s context window.</p>
<p class="md-p">So the projection has a budget, 128 KiB by default. It fills with the most recently read functions first, on the assumption that what the agent read last is what it is working on, and a function is never cut in half. One that does not fit is left out, and its marker says so, so the model knows to read it again if it needs it.</p>
<p class="md-p">Just before sending, FreshCtx re-reads the chosen files and checks the hash of every piece of code it inserts. If a formatter changed the file after the plan, or a tool call was left without its result, the request is not sent. A half-rewritten request is worse than none.</p>
<h2 class="md-heading" id="is-the-code-in-the-request-up-to-date"><span class="md-hashes" aria-hidden="true">##</span> Is the code in the request up to date?</h2>
<p class="md-p">Yes. I replayed every 56th run of the OpenHands dataset, 1,198 in total. For each one, I rebuilt the repository at its starting commit and ran its views, edits and requests through the engine, without calling a model:</p>
<p class="md-p">Zero stale views.</p>
<p class="md-p">The more telling result is the code in their place: <strong class="md-strong">every piece of it matched the rebuilt file byte for byte at the moment of its request</strong>.</p>
<p class="md-p">One gap remains, however: after an edit, OpenHands prints a snippet of the edited file, which FreshCtx does not track yet, so 1,237 requests still carried stale code that way.</p>
<p class="md-p">A separate check covered an edit from outside the agent: a session in <a class="md-link" href="https://github.com/earendil-works/pi" title="https://github.com/earendil-works/pi" rel="noopener noreferrer" target="_blank">Pi</a>, an open-source coding agent, read a file, another process changed a constant from <code class="md-codespan"><span class="md-muted">`</span>10<span class="md-muted">`</span></code> to <code class="md-codespan"><span class="md-muted">`</span>12<span class="md-muted">`</span></code>, and the next request held <code class="md-codespan"><span class="md-muted">`</span>12<span class="md-muted">`</span></code> with FreshCtx and <code class="md-codespan"><span class="md-muted">`</span>10<span class="md-muted">`</span></code> without it.</p>
<h2 class="md-heading" id="does-the-model-actually-do-better"><span class="md-hashes" aria-hidden="true">##</span> Does the model actually do better?</h2>
<p class="md-p">This is the question that matters most, and it cannot be checked byte for byte. I ran two small live experiments with <a class="md-link" href="https://pi.dev/" title="https://pi.dev/" rel="noopener noreferrer" target="_blank">Pi</a> and the <code class="md-codespan"><span class="md-muted">`</span>deepseek-v4-flash<span class="md-muted">`</span></code> model, each with and without FreshCtx:</p>
<ul class="md-list md-list--unordered">
<li class="md-li"><strong class="md-strong">When the model could re-read, it did worse.</strong> Five tasks gave a session two reads, changed both files while it was closed, then resumed it and checked the answer. Without FreshCtx, the model saw old code, re-read both files and answered correctly every time. With FreshCtx, it had current code from the start, but failed two tasks.</li>
<li class="md-li"><strong class="md-strong">When it could not re-read, it did better.</strong> In one ten-turn session, I changed a file between turns and asked about it with the instruction &quot;Do not use tools. Do not read.&quot; On the turns where the code had changed, plain Pi gave the old value 8 times out of 8. FreshCtx gave the current value 7 times out of 8.</li>
</ul>
<p class="md-p">In the first experiment, each dot is one request after the session resumed, and a run stopped after eight:</p>
<p class="md-p">Both failures came from how the code was presented. In the tax-base task, the reads came back as markers, and nothing linked them to the current code at the end, so the model kept re-reading. It only stopped because each run was capped at eight requests, a limit I set so that a stuck model could not loop forever.</p>
<p class="md-p">In the moved-symbol task, it took the <code class="md-codespan"><span class="md-muted">`</span>46<span class="md-muted">`</span></code> in <code class="md-codespan"><span class="md-muted">`</span>fees.js:region:46<span class="md-muted">`</span></code>, a size in bytes, for a line number, and kept asking for a line that did not exist. The header now says <code class="md-codespan"><span class="md-muted">`</span>46bytes<span class="md-muted">`</span></code>; the marker fix is still open.</p>
<p class="md-p">In the second experiment, the model read the file only on turns 1, 5 and 8:</p>
<p class="md-p">This setup clearly favors FreshCtx by design. Together, the two experiments say that FreshCtx helps when the model would otherwise trust the conversation, and that a marker must say where its current code went.</p>
<p class="md-p">Until that is fixed and tested again, I have no evidence that FreshCtx gives better answers in normal use, where the agent can re-read.</p>
<h2 class="md-heading" id="is-it-cheaper"><span class="md-hashes" aria-hidden="true">##</span> Is it cheaper?</h2>
<p class="md-p">The answer is no. Most of each request is an exact copy of the previous one, so providers serve it from the prompt cache at a fraction of the price, about 1/30 in the DeepSeek price table I used. Only the end is billed in full.</p>
<p class="md-p">FreshCtx keeps the cached start stable, because markers do not change, but it puts the current code in the full-price end:</p>
<p class="md-p">The old views that FreshCtx removes would have been cheap, and the code that replaces them is not. Requests also came out 1.93% larger in the replay, because reading a few lines inside a large function brings the whole function back.</p>
<p class="md-p">CORVUS reports up to half the cost, and the two results do not contradict each other. Its savings come from the agent&#39;s behavior: fewer duplicate reads, fewer failed edits and fewer steps, and the CORVUS authors flag caching as an open problem, as I do. </p>
<p class="md-p">My replay cannot show behavioral savings, because the agent&#39;s actions were fixed in the recording, and my live runs were too small to measure them. In the one resumed task where I compared tokens, FreshCtx used more, 6,489 prompt tokens against 2,410, because the model needed five requests instead of two.</p>
<h2 class="md-heading" id="lessons-for-anyone-building-something-like-this"><span class="md-hashes" aria-hidden="true">##</span> Lessons for anyone building something like this</h2>
<ul class="md-list md-list--unordered">
<li class="md-li"><strong class="md-strong">Measure staleness in your own transcripts.</strong> The rule above is a short script, and it needs nothing but the transcripts.</li>
<li class="md-li"><strong class="md-strong">Rewrite the copy, not the record.</strong> Keep what the tools returned for debugging and replay; change only what you send.</li>
<li class="md-li"><strong class="md-strong">Label every number you show a model.</strong> A bare <code class="md-codespan"><span class="md-muted">`</span>46<span class="md-muted">`</span></code> was read as a line number. The header now says <code class="md-codespan"><span class="md-muted">`</span>46bytes<span class="md-muted">`</span></code>.</li>
<li class="md-li"><strong class="md-strong">Leave a pointer when you move content.</strong> The tax-base loop happened because nothing linked the markers to the code at the end.</li>
<li class="md-li"><strong class="md-strong">Put changing content at the end, and budget for it.</strong> The end works best and keeps the cached start stable, but it is paid in full on every request.</li>
<li class="md-li"><strong class="md-strong">Judge by outcomes.</strong> Current code and a finished task are different things.</li>
</ul>
<h2 class="md-heading" id="what-i-take-from-this"><span class="md-hashes" aria-hidden="true">##</span> What I take from this</h2>
<p class="md-p">Keeping the code in a request current is solvable, and it can be checked byte for byte. Whether it helps is less clear. </p>
<p class="md-p">The model did not do better where it could re-read, and it is not cheaper. The harder problem is making sure the model understands what it was given.</p>
<p class="md-p">This is one agent and one model on one public dataset, plus a handful of live runs. Next, I will make each marker say where its current code went and re-run the resumed tasks with repeated runs. </p>
<p class="md-p">If that still brings no gain, FreshCtx is mainly useful where re-reading does not happen: long sessions, compacted histories and edits from outside the agent. If it does, the next question is whether the gain survives the cache cost.</p>
<h2 class="md-heading" id="try-it"><span class="md-hashes" aria-hidden="true">##</span> Try it</h2>
<p class="md-p">From a clone of the <a class="md-link" href="https://github.com/felipebasurto/freshctx" title="https://github.com/felipebasurto/freshctx" rel="noopener noreferrer" target="_blank">FreshCtx repository</a>, with Node.js 22 or newer:</p>
<div class="md-codeblock"><div class="md-codeblock-gutter" aria-hidden="true">```bash</div><pre class="md-pre"><code class="md-code language-bash">npm run demo</code></pre></div>
<p class="md-p">It runs the real engine on the <code class="md-codespan"><span class="md-muted">`</span>total<span class="md-muted">`</span></code> example, without calling a model, and prints the rewritten request with its checks. There are bridges for <a class="md-link" href="https://github.com/felipebasurto/freshctx/tree/main/bridges/pi" title="https://github.com/felipebasurto/freshctx/tree/main/bridges/pi" rel="noopener noreferrer" target="_blank">Pi</a>, tested against the real agent, and <a class="md-link" href="https://github.com/felipebasurto/freshctx/tree/main/bridges/openhands" title="https://github.com/felipebasurto/freshctx/tree/main/bridges/openhands" rel="noopener noreferrer" target="_blank">OpenHands</a>, tested with recorded requests. To connect another harness, see the <a class="md-link" href="https://github.com/felipebasurto/freshctx/blob/main/docs/protocol.md" title="https://github.com/felipebasurto/freshctx/blob/main/docs/protocol.md" rel="noopener noreferrer" target="_blank">protocol reference</a> and the <a class="md-link" href="https://github.com/felipebasurto/freshctx/blob/main/CONTRIBUTING.md" title="https://github.com/felipebasurto/freshctx/blob/main/CONTRIBUTING.md" rel="noopener noreferrer" target="_blank">contributor guide</a>; pull requests are welcome.</p>
<p class="md-p">The measurements, replays and live runs are all in <a class="md-link" href="https://github.com/felipebasurto/freshctx-research" title="https://github.com/felipebasurto/freshctx-research" rel="noopener noreferrer" target="_blank">freshctx-research</a>.</p>
<p class="md-p">Thanks for reading, and thanks to the CORVUS authors, whose paper this work rests on.</p>
]]></content:encoded>
      <category>Coding agents</category>
      <category>Context engineering</category>
      <media:content url="https://felipebasurto.com/assets/blog/model-context-is-not-static/og.png" medium="image" type="image/png" />
    </item>
  </channel>
</rss>
