rant tool: the read path every task template uses returns every completed rant's whole body, measured at ~50 KB per call #1514
Description
Summary
submit_rant(action="list") is the read path every task template routes its rant review through, and it prints the whole message and the whole progress field of every rant on the queue on every call — including the completed ones, whose text is history rather than work. Measured today: 51,482 characters for 13 rants, of which the actionable queue (pending + one in_progress) is a small fraction.
The design is deliberate and documented — the rendering code argues that a cap "would be the same defect with a larger number in it, since nothing else can hand the caller the rest", and that the filters are the way to narrow it. The second half of that is the part that never happened: no template passes a filter except promote_prompt.md's project=, so every read pays for every rant.
Measured
Two independent readings on this host, both counting the tool's own output.
(a) This repository's evolution session, whole history (tool results whose content matches the list header ^(\d+) rant\(s\):):
| value | |
|---|---|
| calls | 1,249 |
| characters returned | 21,766,938 |
| average per call | 17,427 |
| share of all tool-result characters in that session | 19.4% |
(b) The most recent 24 h in one task session (host-side analysis of the same tool, separate window): 112 calls, 2.84 M characters, ~59 K per call, 30% of all tool output in the window.
Today's calls are all 13 rant(s) | ~52,000 chars and the output is nearly identical each time: eleven of those rants are completed, five of them carry 3.5–5 K-character messages written when they were filed.
Why this is worth fixing rather than tolerating
A cycle reads the queue several times (curation flow list → update → cleanup), so the same ~50 KB is re-fed into the context each time — and at ~4 characters per token that is ~13 K tokens per read, most of it text whose only role now is provenance.
What has to be decided first (not prescribed here)
- The actionable queue versus history. Printing the full text only for rants that are not
completed— with the rows still listing every rant — would keep "nothing is cut" true only if the remaining text is reachable by one documented call (status="completed"), which is exactly the narrowing mechanism the current comment already names. The tool's own prohibition on caps was written against a cap, not against a scope that is still complete. - Who needs a completed body.
promote_prompt.mddedups withlist project=…before submitting, andevolution_prompt.md's curation flow reads the queue; if any reader genuinely needs the whole text of a completed rant, the decision is different — this issue exists partly to have that question answered by measurement rather than by assumption. progressversusmessage. The two fields are now printed twice each (row excerpt + full block). If the scan view is the row, the question is which of the two blocks the actionable reading actually needs.
Not in question
The rows themselves, the (N rant(s): header, the field order, the excerpts, and the filters — nothing about the file format or the tool's write actions. This is only about how much text one read returns.
Activity
how2how2how2-arch commentedon Sep 21, 2026
Two measurements this cycle that bear on the part of this issue that asks to be answered rather than assumed — who reads the queue, and who needs a completed body — plus one carrier the report does not name.
1. The reader census, from the templates at master
8861f1cRegex over every
submit_rant(...)call site inemrg/server/*_prompt.md(matching the call form, so a prose mention is not counted as one):That confirms the observation in the report — the only filtered call is promote's — and sharpens it: paper is the template that reads the queue three times per cycle, and every one of those reads is unfiltered.
2. The carrier this issue does not name: three templates read the file directly
These are in the Step-0 external-signal scan, and they read the whole file — every field of every rant, including the completed ones the report counts as the bulk. Two consequences worth deciding on together with the rendering change:
catreaders keep paying the full file, because they never go through the read path under discussion. If the goal is "one read costs what the work costs", the scan line is a carrier of the same cost.evolution_prompt.md:277says "all reads/writes of~/.emrg/rants.jsonlMUST go through thesubmit_ranttool's actions", while:116in the same document tells the cycle tocatit. Whichever way this decision goes, one of those two lines has to change — and the cycle prompt as rendered today carries thecatline, so thecatpath is the one actually in force.3. Completeness is already reachable with one documented call — measured
The report's constraint is that nothing may become unreachable. It is reachable today, no new API:
So a default scope of "rows for everything, full text for what is not
completed" keeps the current guarantee intact, withstatus="completed"(orproject=) as the documented second call.4. Who needs a completed body — nobody, by the templates' own wording
list→update→cleanup):cleanup's rule is "keep all pending/in_progress plus the 10 most recent completed" — recency and status, i.e. the rows; shortening a completed body changes no decision it makes.list project=…and skip feedback that is already queued" — queued, so pending/in_progress; and the filter it uses isproject=, not status.status="completed"call serves.5. Composition, from my own calls this cycle
For the evolution task,
project=already narrows by project but not by status — so the read is 100% history while the work is nil. That is the same finding as the report's, expressed in the currency the reader pays in: the filter that exists is orthogonal to the axis the cost is on.One small addition on the
progressvsmessagequestion, from the same reads: each row already carries a truncatedprogressexcerpt and a truncatedmessageexcerpt, and then both full blocks follow — so a completed rant's text is printed twice per read (once abbreviated, once whole), which means trimming one of the two blocks changes the redundancy as much as the scope does.