Repository navigation
Description
usage_summary's description names the $92 slice as the whole coverage gap and never mentions the $9,494 one — and prescribes a remedy that recovers neither
The claim the tool makes
workers/mcp/src/instance-tools/account.ts:34, last sentence:
payerCoveragesays how far the charged figure's window actually reaches: the earliest call in the range whose payer could be established, and the count and value of the calls BEFORE it, which carry no payer and are therefore absent fromchargedCostMicros. A range wider than that boundary understates charged spend, and by how much ispayerCoverage.unattributedBefore.
What the tool actually returns
usage_summary(range: "30d"), production, 2026-08-15:
"payerCoverage": {
"firstAttributedAt": "2026-08-07 03:35:02",
"attributed": { "calls": 2385, "costMicros": 41004149 },
"unattributedBefore": { "calls": 2975, "costMicros": 91855896 },
"unattributedSince": { "calls": 436, "costMicros": 9494469266 }
},
"unmetered": { "drives": 4, "aiCliDrives": 1, "instances": 1, "lastAt": 1786507310365, "windowDays": 14 }| slice | calls | USD | documented? |
|---|---|---|---|
| attributed (charged) | 2,385 | $41.00 | yes |
unattributedBefore |
2,975 | $91.86 | yes — named as "by how much" |
unattributedSince |
436 | $9,494.47 | no |
unmetered |
— | — | no |
unattributedSince is 103× larger and is 99.04% of the unattributed value. The arithmetic is exact, so the numbers are right and the sentence is wrong:
41,004,149 + 91,855,896 + 9,494,469,266 = 9,627,329,311=totals.costMicros, to the micro.91,855,896 + 9,494,469,266 = 9,586,325,162=byPayer.unknown.costMicros, exactly.
Why the wording matters more than a missing field
The described gap is a window problem, and its implied remedy is to narrow the range. Narrowing recovers none of the $9,494: unattributedSince is by definition the rows that sit alongside attributed ones, inside any window you choose.
The API knows this. workers/api/src/lib/usage-coverage.ts:64-70, on the field the description omits:
* No payer, and NOT older than {@link firstAttributedAt} — so, alongside calls we could attribute.
*
* A different problem with a different remedy: a coding engine on a machine login (#551), which
* one stored token fixes.
and the console makes the same split deliberately — store/console/src/lib/usageFigures.ts:104-108:
NULL has two causes (older than the column, or a coding engine on a machine login) and they are not distinguishable per row. Blaming the date for all of it would replace one confident wrong number with another, on an account where the second cause is 99.6% of the value.
So the API layer and the console both got this right when #544 landed, and the MCP description — written in the same commit, 1fcac9e — describes only the half the console's chargedCoverageNote covers. A caller reading it does exactly what that comment warns against: blames the date for all of it.
Not a duplicate of #551, and where the line is
#551 is the root cause — machine-login → NULL payer, one stored claude setup-token resolves it — and it is open with the same numbers. This is a separate documentation defect on a different surface, and it survives #551 being fixed: even at zero unattributed spend, the description would still name only one of the two slices and still omit unmetered. Fixing #551 makes the number small; it does not make the sentence true.
This issue should be closed by editing account.ts:34. It should NOT be closed by fixing #551.
What to do
Replace the last sentence of the description. Suggested text, ready to paste:
payerCoveragesplits the range into three, and the split matters because the two gaps have different remedies.attributedis whatchargedCostMicroscounts.unattributedBeforeis calls older thanfirstAttributedAt— the payer column did not exist yet, so a wider range understates charged spend by that much and a narrower one does not.unattributedSinceis calls alongside attributed ones whose payer still could not be established — almost always a coding engine running on the machine's own login; narrowing the range recovers none of it, and one storedclaude-codesign-in token converts it (#551). Read both before concluding anything about spend: on a heavy coding accountunattributedSinceis usually the larger by two orders of magnitude.unmeteredreports work the platform did not meter at all (tmux/CLI drives it did not observe), as counts over the lastwindowDays, not as value.
Alternatives considered and rejected
- Drop
unattributedSincefrom the response. It is the honest number and the console consumes the type. Removing it makes the surface agree with a wrong sentence. - Fold both into one
unattributedtotal. Loses the only thing that distinguishes "impossible to recover" from "one token away", which is the distinctionusage-coverage.ts:64-70exists to preserve.
Acceptance criteria
- The
usage_summarydescription names all threepayerCoverageslices andunmetered. - It states that narrowing the range does not recover
unattributedSince. - A test asserts every top-level key the
/v1/usageresponse can contain appears in the tool's description string — so the next field added cannot ship undocumented. (This is the actual fix; the wording is the symptom.)
Regression risk
None to behaviour — description-only. The risk is the opposite one: a longer description eats context on every tools/list. #561 just added SERVER_INSTRUCTIONS and annotations to that same payload, so keep the replacement roughly the length of the sentence it replaces rather than adding a paragraph.
Related: #551 (root cause, open), #544 (payerCoverage shipped, commit 1fcac9e), #561/#562 (the tools/list payload this description rides in).
Activity
fix(mcp): usage_summary names every field /v1/usage returns, and a te…
serge-ivo commentedon Aug 15, 2026
Landed as
0c0d8ad, deployed (Deploy MCP Worker green, smoke test passed,/health→{"ok":true,"service":"proagentstore-mcp","tools":135}).What shipped
workers/mcp/src/instance-tools/account.ts— theusage_summarydescription now names all threepayerCoverageslices plusfirstAttributedAtandunmetered, says narrowing the range recovers none ofunattributedSince, and names #551 as the cause. 1076 → 1355 chars (+279).The real deliverable is AC 3, and it is derived rather than typed (
workers/mcp/src/instance-tools/account.test.ts):aggregateUsage([])supplies its own 8 keys,payerCoverage([])its own 4 sub-keys, and the 2 the route adds outside the spread (range,unmetered) are parsed out of thereturn c.json({...})literal inworkers/api/src/routes/usage.ts. Per ADR 0002 every step asserts its denominator first (≥8 from the aggregator, ≥2 from the route, ≥10 combined, ≥4 sub-keys), and the spread is asserted to be...summarybound toaggregateUsage(— otherwise the test would measure a function the route no longer calls and pass while documenting a shape nobody returns. Membership is on backticked spans split on., not substrings, becauseattributedis a substring of bothunattributedBeforeandunattributedSince.Watched red on the pre-fix description: "undocumented /v1/usage keys (of 10 derived): range, unmetered, daily, byModel, byKind, byPayer" and "undocumented payerCoverage fields (of 4): firstAttributedAt, attributed, unattributedSince".
Two claims in this issue's suggested wording were NOT taken
Both contradict the module they describe, and a fix for a description that asserted something untrue must not assert two new things.
attributedis whatchargedCostMicroscounts" is false.usage-coverage.ts:118-126gatesattributedonhasPayerand the charged figure onisCharged, and calls that difference "the distinction the whole module turns on" — asubscriptionrow is attributed and charged to nobody.attributedis a strict superset. The description now says so and namessubscription, which the rewrite had otherwise dropped (the old text's "a coding engine on a subscription" was its only mention of that case). Pinned by a test that derives the divergence instead of asserting the prose: a subscription row must be counted attributed bypayerCoverageand rejected byisCharged, and only then is the description required to name it. Watched red: "the description must namesubscription— the payer that is attributed and charged to nobody … expected false to be true".unattributedBeforeis calls predating the payer column" is the confident causeusage-coverage.ts:24-28refuses. The two NULL causes are indistinguishable per row, and a machine-login row older than the boundary lands in that slice too. Both slices are now defined byfirstAttributedAtand nothing else, stating only the mechanical consequence. For the same reason "almost always a coding engine on the machine's own login" is softened to "typically": 99.6% is a measurement of one account, quoted as such inusageFigures.ts, not a property of every account.Sharper than the issue
Five of the six keys the new test found undocumented had nothing to do with
payerCoverage.daily,byModel,byKind,byPayerandrangewere never named at all — the old opening described the response in English ("broken down by agent, agent INSTANCE, model, activity and PAYER") and named none of the keys a caller has to index into. That is invisible until the check is mechanical, which is the argument for AC 3 being the deliverable rather than the wording. Naming the breakdowns by key in the opening also paid for AC 3 out of text that already existed: this is +279 chars where the issue's ready-to-paste paragraph was +536 for the last sentence alone and still left those five unnamed.Left open deliberately — the "should the shape change too?" question
Not acted on, and I think the answer is "no, not here":
usage-coverage.ts:24-33argues correctly that the two NULL causes are indistinguishable per row and that collapsing them replaces one confident wrong number with another, andunmeteredis in counts rather than value because there are no tokens to price. The console reads the same shape and gets it right. The one thing that might warrant restructuring is thatpayerCoveragesits at top level as a peer of the breakdowns rather than nested under thetotalsit qualifies — but that is an API change with a live consumer, not a side effect of a doc fix. Worth its own issue if anyone wants it.Not verified
The description itself is only observable through an authenticated
tools/list; unauthenticatedPOST /mcpinitializereturns401 invalid_token, so I could not read it back off the wire. Verified instead: the deploy is green and smoke-tested,/healthserves 135 tools, and the test asserts the string's content against the API's own shape. A connected MCP client should see the new text on nexttools/list(hosts cache it — see #573).feat(mcp): the published surface is hashed against the version that p…