DEV Community

Andre
Andre

Posted on Originally published at olund.dev

81% of my agent's telemetry was a copy of something I already had

Every side-effecting tool call my agent makes gets written down. A hook
fires after the tool returns, and one row lands in the episodic store on
disk: what the tool was, what it was asked to do, what it said back. The
row is scrubbed for secrets on the
way in, indexed for search, and pruned after thirty days.

Two thirds of the largest rows in it were a copy of a file I already had
in git.

The harness hands back the entire edited file as an Edit tool's response,
so the row I write is sized by the length of the file rather than by the
size of the edit. Changing one line of a long file costs 16 KB of
telemetry. Rewriting half of a short one costs a fraction of that. Two
thirds of my Edit rows sat pinned against the cap for that reason alone,
and every byte of what they held was already recoverable twice over: the
old and new strings are in the row's own input field, and the resulting
file is in git.

The absolute numbers here are small and I would rather say so than have
you notice: one developer's machine, 82 MB of capture in the measured
week, a store that had reached a quarter of a gigabyte. Nobody's disk is
in danger. The reason I care is that the instinct that writes a
redundant 16 KB into SQLite is the same one that writes it into a context
window, a log pipeline, or a bill, and those have real prices. The
question is not what capture costs. It is which captured bytes have a
reader.

The design took one sitting and the caps in it were guesses. Inputs at
64 KB, responses at 16 KB, one number for every tool, because at design
time nobody had measured which responses carried information. Guessing
was fine then. What made me stop guessing was the day the thirty-day
prune landed and the store turned out to be holding 51.6 thousand
tool-call rows and 266.1 MB of row text. A prune only bounds how long a
row lives; it says nothing about what a day of capture costs. The steady
state was thirty days of verbatim payloads, whatever that turned out to
be, forever.

The census comes before the measurement

Before measuring bytes I wrote down who actually reads them, because that
determines whether the bytes are worth anything at all.

The full-text index covers the summary field only. Every one of those
megabytes of payload is unsearched, which means no cap I choose can
change recall by a single row. No automated consumer touches tool-call
payloads either: the consolidation cycle ingests
specific event types and tool calls are not among them, the notes route
in the GUI excludes them by name as high-volume machine noise, and the
memory search tool surfaces summaries. The single reader is me, by hand,
running episodic show <id> when I want to know what actually happened
in a session that went wrong.

One human reader, used rarely. That reframed the whole thing. It was
never a disk-mechanics problem with a tuning answer. It is a policy
question: what about a session has to stay observable after the fact, and
what is just being hoarded.

Four options, all wrong the same way

I drafted four arms before running any numbers.

Stop capturing responses entirely, keeping only what each tool was asked
to do. Tighten the response cap from 16 KB to something like 2 KB, so
every response keeps a taste. Skip read-only Bash commands, which are
most of the row count. Or do nothing, live with roughly 300 MB, and keep
running the prune by hand.

The measured week ran 2026-07-25 to 07-31: 20,778 tool-call rows, 82.2 MB
of row text, a mean of 2,968 rows and 11.7 MB a day. Load is spiky, so
the median day matters too: 1,853 rows, 7.2 MB. Responses were 66.5 of
the 82.2 MB. Eighty-one percent of everything the hook wrote was the
response half.

Splitting that by tool is where the four arms fell apart.

tool rows mean response at the 16 KB cap
Bash 18,137 1,797 B 157 (0.9%)
Edit 2,111 16,468 B 1,429 (68%)
Write 405 4,978 B 18 (4%)

The pinning from the opening has a counterpart here, and the pair is the
finding: the same 16 KB cap binds on 1,429 of 2,111 Edit rows and on 157
of 18,137 Bash rows. Almost always for one tool, almost never for the
other.

So the four arms had all made the same mistake, and it was not a mistake
about numbers. Each of them priced tool_response as one population. It
is two, and their economics run opposite ways.

The Edit and Write echoes cost 35.1 MB a week and carry nothing that is
not already in the input field and in git. Keeping them means storing a
third copy of something I have twice.

The Bash and MCP responses cost 31.4 MB a week and carry everything.
That is what a command actually printed: the build that failed, the
assertion text, which branch the script took when it mattered. Nothing
else in the system records any of it. Drop it and the event is gone.

An expensive redundant half and a cheap irreplaceable half. Any single
cap prices them the same and is therefore wrong on one of them by
construction, in whichever direction you move it. Dropping all responses
saves the most and deletes the only forensics I have. A uniform 2 KB cap
saves 52% and starts biting exactly the Bash outputs worth keeping,
because their mean is already 1,797 bytes.

The fifth arm

The measurement produced a fifth option I had not drafted, and that is
the part of this I would repeat. Split the policy by tool: drop the
response for the file-mutating family entirely, keep everything else at
4 KB.

That lands within 2 MB a week of the aggressive uniform cap, and instead
of degrading the forensic surface it improves it. The Bash response I
actually read keeps both of its ends at 4 KB. The Edit echo I have never
once read goes to zero.

The rule underneath it fits in a sentence: a tool response is kept only
where nothing else records it. Default is keep. A tool joins the drop
list when its response is demonstrably recoverable somewhere else, and
"demonstrably" means you can name the other place.

Two small decisions came with it, both cheap and both load-bearing.

A dropped response is written as an explicit null rather than omitted.
The payload contract stays shape-stable, so a reader spanning the cutover
sees one schema with two values instead of two schemas.

And the kept responses truncate head and tail, not head only, splitting
the budget between both ends with a marker in the middle. A build log
carries the command at the top and the error summary at the bottom.
Head-only truncation reliably throws away the second one, which would
have made the surviving half worse than what it replaced.

The implementation is a constant, a predicate, and a truncation mode.
The tests pin the shape rather than the size: an Edit row's response is
an explicit null with the file echo absent, and a 15 KB build log keeps
both of its ends.

Three weeks later

The verification was set for one full retention window on the new shape,
and then left alone.

The cutover is visible in the data instead of inferred from an install
timestamp, which matters more than it sounds: the hook shells out to the
installed binary, so the old shape keeps writing until the resident stack
is actually rebuilt and restarted. Edit rows start carrying a null
response on 2026-08-01, four of 184 that day, and from 08-02 every single
one of them does. So the clock started 08-02.

The before rows are that same diagnostic week, recomputed at
verification time and run a day further to meet the cutover, so they come
out a little larger than the 82.2 MB above. The daily rate reproduces:
11.46 MB against 11.7.

rows MB of which response bytes/row
before, Edit 2,295 39.19 35.84 17,904
before, Bash 18,544 40.46 31.60 2,288
before, Write 428 4.03 2.05 9,862
before, all 21,395 84.04 69.81 4,119
after, Edit 3,735 5.01 0.00 1,407
after, Bash 38,221 78.36 57.84 2,150
after, Write 1,301 6.86 0.00 5,533
after, all 43,795 90.97 58.34 2,178

Each half did what the split predicted. Edit is down 92% per row and
Write 44%, both with the response gone. Bash is down 6%, which is the
16 KB to 4 KB cap trimming a tail off a population that was already
small. Nothing regressed.

Now the part I had to be careful about.

The obvious headline is 4.33 MB a day against the old 11.7. That number
is confounded and I nearly published it anyway. The two windows were not
equally busy: 2,968 rows a day before, 2,085 after. Some of that
improvement is just three quieter weeks. The honest comparison is per
row, where the same bytes get divided by the same denominator: 4,119 to
2,178, down 47%. Adjusted back to the old activity level it comes to
6.16 MB a day against the 5.9 the arm projected, which is a 4% miss on a
projection built from a single week. I will take that as agreement.

One thing no table here can tell me. In the month since the cutover I
have not once gone looking for an Edit response and found it missing.
That is the outcome I wanted, and it is also the weakest evidence in this
post: an absence I never reached for looks exactly like an absence I
failed to notice. The weight is still carried by the structural argument,
that what the echo held sits in the input field and in git. Living with
it has only failed to contradict that.

The number that did not move

At the verification the store held 273.63 MB across 80,674 rows. Before
any of this started it was 266.1 MB. It had gone up.

Neither reason was the write shape. Eight pre-cutover days were still
inside the thirty-day window on that date, and they leave on their own.
And the prune is deliberately a verb I run, not a daemon, with a due
signal in the status output telling me when it is worth running. In the
three weeks after the change landed I ran it exactly once, so 13,638
rows past the window were sitting on 89.25 MB, waiting for me.

I closed the task there rather than hold it open to watch a subtraction
finish. The write rate was the half this work owned, it was met, and it
could not un-meet itself. The rest was arithmetic on that rate plus a
prune I keep forgetting to run, which is a different problem with my
name on it.

I checked again before publishing this, a fortnight later and with the
prune since run: 53,649 tool-call rows, 119.16 MB, against the 266.1 MB
it started at. The thirty-day window is now entirely on the new shape,
which makes it the first clean reading of the thing the arm was picked
for. It holds 118.87 MB across 53,579 rows, at 1,786 rows and 3.96 MB a
day. The projection was around 130 MB for a full window. Doing nothing
projected 352.

Per row that is 2,326 bytes against the 4,119 the old shape wrote, so
44% off rather than the 47% the day-21 read showed, which is what a
window covering a busier stretch should look like. Every Edit row in it
carries a null response, 4,315 for 4,315.

Two things I would keep from this. The reader census was worth more than
the byte measurement: knowing that one human reads these payloads rarely,
and that no index depends on them, is what turned a tuning exercise into
a question I could actually answer.

And a total tells you a problem exists without telling you what it is.
Eighty-one percent is what got me to open the query. It took splitting by
tool to find that two thirds of the expensive rows were a file already
sitting in git, and only that second number implied what to do.

Top comments (0)