Token Budget in Multi-File Refactoring: Sequencing Over Squeezing
Cross-cutting refactors are the one workload where context budget stops being an optimisation and becomes the thing that decides whether the work is possible.
Refactoring is the task that exhausts the window
Most agent tasks touch a handful of files. A cross-cutting refactor touches dozens, and it needs to hold enough of them in view simultaneously to keep the change coherent. That is the one workload where context budget stops being an optimisation and becomes the binding constraint.
The symptoms are recognisable. Early files are transformed one way and later files another. The agent forgets a decision it made twenty files ago. It starts re-reading things it has already read. Output quality degrades in the second half of a long session in a way that has nothing to do with the difficulty of the remaining work.
Where the budget actually goes
| Consumer | Typical share | Reducible? |
|---|---|---|
| Files read in full | 50–70% | Yes — substantially |
| Tool results (grep, test output) | 10–25% | Yes — often dramatically |
| Conversation history | 10–20% | Partially |
| System prompt and tool definitions | 5–15% | Somewhat — disconnect unused servers |
| Your context files | 1–5% | Yes, but it is the smallest line |
People optimise the last row because it is the one they control directly. The first two rows are where the tokens actually are.
Read less of each file
An agent needing to know a module's public shape does not need its implementation. A signature listing is a fraction of the size and frequently sufficient.
#!/usr/bin/env python3
# outline — public surface of a module, no bodies
import ast, sys
src = open(sys.argv[1], encoding="utf-8").read()
for node in ast.parse(src).body:
if isinstance(node, ast.ClassDef):
print(f"class {node.name}:")
for sub in node.body:
if isinstance(sub, (ast.FunctionDef, ast.AsyncFunctionDef)):
args = ", ".join(a.arg for a in sub.args.args)
print(f" def {sub.name}({args})")
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
args = ", ".join(a.arg for a in node.args.args)
print(f"def {node.name}({args})")
On a 600-line module this returns perhaps 25 lines. Across the twenty peripheral modules a refactor touches only at the boundary, that is the difference between fitting and not fitting.
## Reading efficiently
- `./outline <file>` — signatures only. Use this when you need
a module's interface, not its implementation.
- Read a file in full only when you are about to change it.
Cap what tools return
Tool output is the most wasteful line item because it is unbounded by default. A grep matching 200 lines returns 200 lines, most of which are irrelevant.
## Searching
- Always bound output: `rg -n --max-count=5 'pattern' src/`
- For counts, not content: `rg -c 'pattern' src/`
- Test failures: `pytest -q --tb=line` — one line per failure,
never full tracebacks during iteration
- Logs: pipe through `tail -50`. Never cat a whole log file.
The test-output line is worth singling out. A full traceback across twelve failing tests can be several thousand tokens where the useful content is twelve lines. Nothing is lost by summarising during iteration — the agent can ask for the full trace on the one failure it decides to investigate.
Sequence the work so context can be discarded
The structural fix, and the one that matters most. Instead of holding forty files at once, arrange the refactor so each stage is self-contained.
Stage 1 — survey (cheap, read-only)
outline every affected module; produce a written plan naming
the new interface and every call site to change.
OUTPUT: refactor-plan.md
Stage 2..n — one module per session
input: refactor-plan.md + that module + its direct dependencies
work, verify, commit
context from stage n-1 is NOT needed
Final — integration
run the full suite, reconcile anything the plan missed
The plan file is what makes this work. It carries the decisions forward across sessions at a fraction of the cost of carrying the files, and it doubles as the reviewable artifact a human can check before any code changes.
For any refactor spanning more than a handful of files, spending one cheap read-only session producing a plan is the highest-return step available. It converts one enormous session into several small ones and gives you a decision point before anything is written.
What not to bother optimising
Some commonly suggested economies are not worth the effort, and it is worth saying so, because they consume attention that belongs elsewhere.
- Shortening your context file below about 400 tokens. You are optimising the smallest line item while files-read dominates. Cut it to what earns its place and stop.
- Asking the agent to be concise. Its output is a small fraction of the total, and terser reasoning frequently produces worse work.
- Stripping comments from files before reading. Comments are some of the highest-value tokens in a file — they carry intent that the code does not.
- Minifying or compressing code before sending it. Saves tokens, destroys the structure the model uses to understand the code, and is consistently a bad trade.
The pattern in all four: they optimise a number without asking what the tokens were buying. Reading the wrong file cheaply is not an improvement over reading the right file expensively.
Restart deliberately
Long sessions degrade. The signal is behavioural rather than numerical: when the agent starts re-reading files it read earlier, or repeating a decision it already made, the accumulated context has stopped helping and started competing.
The move is a fresh session seeded with the plan and a short handoff, not a continuation. This feels wasteful — the agent "understands" the codebase now — and that understanding is exactly what has become expensive to carry.
Measuring rather than guessing
Everything above is a hypothesis about where your tokens go. Checking is cheap, and the answer differs by codebase.
#!/usr/bin/env python3
# Rough breakdown from a session log with tool calls recorded.
import json, sys, collections
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
totals = collections.Counter()
for line in open(sys.argv[1], encoding="utf-8"):
ev = json.loads(line)
n = len(enc.encode(ev.get("content", "")))
if ev["type"] == "tool_result":
totals[f"tool:{ev['tool']}"] += n
else:
totals[ev["type"]] += n
grand = sum(totals.values())
for k, v in totals.most_common():
print(f"{v:8d} {100*v/grand:5.1f}% {k}")
print(f"{grand:8d} 100.0% TOTAL")
Run it against one large refactoring session. The usual surprise is a single tool — an unbounded grep, or full test tracebacks — accounting for more than every source file combined. That is a one-line fix in your context file, and you would not have guessed which tool it was.
An order of operations
- Disconnect MCP servers you are not using in this session.
- Run a read-only survey; produce a written plan.
- Confirm the plan yourself before any code is written.
- One module per session, seeded with the plan.
- Outline peripheral modules; read in full only what changes.
- Bound every search and test invocation.
- Commit at each stage, so a bad session is cheap to discard.
Step seven is the safety net. Small committed stages mean an unproductive session costs one module rather than a day, which is what makes the whole approach tolerable to work with.
Files read in full and unbounded tool output are where the budget goes — not your context file. Read signatures instead of implementations for peripheral modules, cap every search and test invocation, and above all sequence the work through a written plan so each session can discard the previous one's context instead of carrying it.