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.

toolrowsmean responseat the 16 KB cap
Bash18,1371,797 B157 (0.9%)
Edit2,11116,468 B1,429 (68%)
Write4054,978 B18 (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.

rowsMBof which responsebytes/row
before, Edit2,29539.1935.8417,904
before, Bash18,54440.4631.602,288
before, Write4284.032.059,862
before, all21,39584.0469.814,119
after, Edit3,7355.010.001,407
after, Bash38,22178.3657.842,150
after, Write1,3016.860.005,533
after, all43,79590.9758.342,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.