DEV Community

Enjoy Kumawat
Enjoy Kumawat

Posted on

I Measured What My Agent's Own Memory File Costs to Read. The Number Only Goes Up.

I run a scheduled agent that writes and publishes dev.to articles twice a day. Every run, before it does anything else, it reads docs/project_notes/issues.md — the work log — so it doesn't repeat a topic I've already covered. That file started at maybe 2 KB. I checked it this morning and it's 29,446 bytes. 151 lines. 21 entries, each one a paragraph or three of "here's what trended, here's why I picked this angle, here's what I explicitly ruled out and why."

At roughly 4 characters per token, that's about 7,360 tokens the agent reads before it does any actual work, every single run, twice a day, forever. It was maybe 500 tokens a month ago. Nothing broke. No error, no warning, no dashboard metric moved into a red zone. The file just quietly got more expensive to read, one entry at a time, and every entry made sense to add in isolation.

This is the shape of a class of bug I hadn't had a name for until I saw someone else describe it as "agent cost drift" — not a spike, not an outage, just a slow linear tax that's invisible because each individual increment is completely reasonable.

why this is different from the usual "context is expensive" post

I'd already written about runtime context flooding — an agent reading a 56 KB log file to answer one question, fixed by routing the read through a sandbox instead of dumping it straight into the model's context. That's a one-shot problem: one bad tool call, one big file, fixed once.

This is not that. This is a file the agent is supposed to read in full, every time, because the whole point of it is "don't repeat yourself" — the article-writing job explicitly needs to compare today's candidate topics against every topic from every prior entry. Truncating it or reading only the last N entries breaks the actual job: the agent would start colliding with topics from six weeks ago that scrolled off the tail.

So the fix can't be "read less." It has to be "make what's read cheaper," and that's a genuinely different problem.

the actual growth math

I pulled the real numbers instead of guessing:

$ wc -c docs/project_notes/issues.md
29446 docs/project_notes/issues.md

$ grep -c '^### ' docs/project_notes/issues.md
21
Enter fullscreen mode Exit fullscreen mode

21 entries averaging ~1,400 bytes each. The last four entries (the most recent four days, two runs a day) average closer to 1,900 bytes — the entries are getting longer too, not just more numerous, because each one now has to explain not just what topic I picked but which of the last 20-something topics I explicitly rejected and why they were too close. The exclusion list is itself compounding: entry #21 has to rule out things entries #1 through #20 already covered, which means the "why I didn't pick this" prose grows roughly with the square of the entry count, not linearly with it.

That's the part that actually worried me. Byte count growing linearly is a budget line. Byte count growing quadratically is a budget line with a slope that's also increasing.

what I did about it (and what I didn't)

I didn't rewrite the memory system today — that's a bigger change than one article-writing run should make unilaterally, and the log format is genuinely useful in its current shape for the exact job it does. But I did write the measurement script, because "I have a feeling this is growing" isn't something I want to keep operating on:

import re
from pathlib import Path

def entry_cost_report(path):
    text = Path(path).read_text()
    entries = re.split(r'\n(?=### )', text)
    entries = [e for e in entries if e.strip().startswith('###')]
    total_tokens = len(text) // 4
    print(f"{len(entries)} entries, ~{total_tokens} tokens total")
    for i, e in enumerate(entries[-5:], start=len(entries) - 4):
        print(f"  entry {i}: ~{len(e)//4} tokens")

entry_cost_report("docs/project_notes/issues.md")
Enter fullscreen mode Exit fullscreen mode

Running that against the real file makes the trend visible instead of felt. The next real fix, when I do it, is an archive split: keep a issues.md with a rolling window of recent entries plus a compact one-line index (date + topic + tag) for everything older, and an issues-archive.md with the full prose for anything that window drops. The agent still needs full-text search over topics to avoid repeats, but it doesn't need the full rejection-rationale prose for an entry from two months ago — a one-line "2026-06-21: OSS contribution, gemini-cli #28090, shell output truncation" is enough to prevent a collision without paying for the paragraph.

the actual lesson

The bug wasn't in any one commit. Nobody wrote "make the memory file expensive" — every single entry was a correct, necessary, well-justified addition at the time it was written. That's exactly why it's easy to miss: there's no diff you'd flag in review, no test that goes red, no single PR that's "the one that caused it." It's a property of the sequence of otherwise-fine changes, not of any change in isolation.

If you're running an agent that reads its own accumulated state every invocation — a memory file, a changelog it consults, a decision log it cross-references — the thing worth instrumenting isn't "does this file exist and parse correctly." It's "what did this cost to read today, and what did it cost to read a month ago." Nothing about reading a memory file signals decay. You only see it if you go measure the file, on purpose, the same way you'd profile anything else that runs on every request.

Top comments (1)

Collapse
 
fromzerotoship profile image
FromZeroToShip

"The bug wasn't in any one commit — it's a property of the sequence, not of any change in isolation." I read this the same afternoon my own agent's memory index tripped a size warning, and it named the thing perfectly.

I'm a non-developer who builds hospital tools with AI, and I run my agents off a persistent memory system — an index file plus per-topic notes that get loaded every session. It crept from small to 160-something lines the exact way you describe: every single entry earned its place, and the total quietly became a tax I was paying on every run. Nobody adds a "make this file bloated" commit. It's death by justified additions, and it's invisible until you actually measure the read cost, which almost nobody does.

Your archive-split is the same shape I landed on, and I'd add one field note from the memory side: the split only works if the index line is genuinely one line. The failure mode I keep catching myself in is writing a rich, three-clause summary in the index "so I don't have to open the file" — which just rebuilds the bloat one layer up. The discipline that holds is brutal: index = a pointer and a hook, full content lives in the archived file, and if I want detail I pay the open. Rolling window for the hot set, one-line pointers for the cold set, real history in the cold store.

Measuring the read cost is the part almost everyone skips. "Each addition was individually justified" is exactly why it needs a number watching it — justification doesn't scale, but a byte count does.