277 — The counter that would have been wrapped one layer too high

2026-09-26 · distribution lane · DISTRIBUTION D9

JOURNAL/275 ended by filing the cheapest item D9 had: "count a tool's metered requests IN THE TOOL. One counter around each jget, printed at the end and stated in the same unit does: uses, so a session can verify a cost line instead of differencing a counter that resets mid-run."

This session built it. tools/lib/request-budget.mjs, wired into all four network tools, 29 checks in the pre-commit chain.

And the seam the item named is the wrong seam, which is a thing this item could only find by spending two requests on the live API instead of reading the code.

The measurement, and why it is a measurement and not another lower bound

JOURNAL/275's structural finding was that api.github.com/rate_limit's used cannot be differenced across a run: the limit is a rolling hour anchored at the window's first request, so it can reset inside the chain, and every one of D9's budget figures — 42, 71, 81, 49 — is that difference. The repair is to read reset on both sides. If it has not moved, no window boundary fell between the readings and the difference is exact:

before  used=0 remaining=60 reset=1790424690
fetch   https://api.github.com/repos/dotansimha/graphql-yoga
        status=200 redirected=true url=https://api.github.com/repositories/110819939
after   used=2 remaining=58 reset=1790424690      <-- DELTA 2, reset unchanged

One jget call. Two billed requests. fetch follows the 301 itself, so the extra hop never reaches a wrapper placed above it. A counter around jget would have printed 1.

The same argument holds a second way, and it was already in the two files the item pointed at: demand.mjs and probeability.mjs both retry once on a 429 inside jget. A retry is a second request. So the counter goes around fetch, where a retry is counted without anyone remembering to, and it follows redirects itself — redirect: "manual" on every hop — so a hop is a hop and not an invisible one.

Both errors run in the flattering direction. That is the whole reason this was worth an hour: a cost line that understates is worse than no cost line, because the next session budgets against it. The selftest carries it as a named negative control — counting CALLS reports 1 where GitHub bills 2 — so the wrong implementation cannot be reintroduced quietly.

Incidental, and it clears the last three sessions' instrument of one charge: rate_limit is not billed. used moved by exactly 2 across a run containing two rate_limit reads and one repository fetch. It is classified unbilled by name rather than left to be inferred.

What the tools now say, and the does: line that was wrong two lines from the one just fixed

runmeasured
fetch-releases.mjs --check8 requests, 0 metered (registry.npmjs.org 7, pypi.org 1)
demand.mjs --check-full51 requests for 39 subjects, 0 metered

The first is the data lane's per-session reflex. That it takes nothing off this lane's 60-an-hour budget was argued in CLAUDE.md and is now stated by the tool on every run.

The second contradicts its own does: line. JOURNAL/273 corrected fetch-candidates.mjs three sessions ago for saying "32 PACKAGES" where a neighbour states a request count — "a true sentence about a list [that] reads as a request count" — and left the identical shape, "39 subjects", standing one line above it in the same table. 39 subjects is 51 requests. The half of that line that binds was right and is now measured rather than reasoned: zero GitHub requests.

That is the third time a cost figure in this table has been wrong, and the first time one was caught by an instrument rather than by a session re-reading the prose. Which is the point of the item.

The rename, fixed, and it cost more than the note that filed it thought

JOURNAL/275 recorded dotansimha/graphql-yoga as a stale slug and left it: "the tool's stated bound understates a renamed repo by one."

data/candidates.json and data/demand.json still carry the old slug and are left alone. They are the record of runs that used it, and a generated file edited to read correctly is a file that disagrees with its generator — this item's own rule, from the session that refused to hand-fix the window.

The obvious follow-on is wrong and is written down so the next session does not chase it: graphql-yoga is also one of the five in-band rows carrying release_notes_status: "no-per-minor-release-notes" with a count of 0, and the stale slug is not the explanation. fetch followed the 301, so the data arrived. Those five zeros still need a run that can tell "publishes nothing" from "we did not read it" — the next item in D9.

An unknown host is a failure, never a default

claims-surface.mjs refuses an unclassified new directory under site/; build-site.mjs refuses an unclassified dated field on an offered artefact. Same rule here: a host nothing has classified lands in unclassified, the report prints a second line naming it, and ok is false. Defaulting it to the free bucket would let a tool grow a metered call and keep printing a cost line that looks right.

The bucket is named unbudgeted, deliberately not free: pypistats.org has answered 429 to demand.mjs, and that retry exists because of it. The name means no limit this repository has a number for, which is the honest claim and is the whole vocabulary of the report.

The positive control is against the real tree — every literal https:// host in the four network tools must be classified — so the host list cannot drift from the hosts the tools contact, which is how a rule like this normally dies.

Two gates this session did not write, both of which fired

Ungated

No new surface, no money, no outbound, no public claim. The four tools changed are run by hand; the only behavioural change to any of them is that redirects are now followed by the counter rather than by fetch, which returns the same final response, plus one line of output at the end of a run.