Skip to content

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 #565

Closed

Description

@serge-ivo

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:

payerCoverage says 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 from chargedCostMicros. A range wider than that boundary understates charged spend, and by how much is payerCoverage.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:

payerCoverage splits the range into three, and the split matters because the two gaps have different remedies. attributed is what chargedCostMicros counts. unattributedBefore is calls older than firstAttributedAt — the payer column did not exist yet, so a wider range understates charged spend by that much and a narrower one does not. unattributedSince is 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 stored claude-code sign-in token converts it (#551). Read both before concluding anything about spend: on a heavy coding account unattributedSince is usually the larger by two orders of magnitude. unmetered reports work the platform did not meter at all (tmux/CLI drives it did not observe), as counts over the last windowDays, not as value.

Alternatives considered and rejected

  • Drop unattributedSince from 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 unattributed total. Loses the only thing that distinguishes "impossible to recover" from "one token away", which is the distinction usage-coverage.ts:64-70 exists to preserve.

Acceptance criteria

  • The usage_summary description names all three payerCoverage slices and unmetered.
  • It states that narrowing the range does not recover unattributedSince.
  • A test asserts every top-level key the /v1/usage response 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

  1. added
    bugSomething isn't working
    documentationImprovements or additions to documentation
    on Aug 14, 2026
  2. serge-ivo commented on Aug 15, 2026

    @serge-ivo
    ContributorAuthor

    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 — the usage_summary description now names all three payerCoverage slices plus firstAttributedAt and unmetered, says narrowing the range recovers none of unattributedSince, 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 the return c.json({...}) literal in workers/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 ...summary bound to aggregateUsage( — 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, because attributed is a substring of both unattributedBefore and unattributedSince.

    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.

    1. "attributed is what chargedCostMicros counts" is false. usage-coverage.ts:118-126 gates attributed on hasPayer and the charged figure on isCharged, and calls that difference "the distinction the whole module turns on" — a subscription row is attributed and charged to nobody. attributed is a strict superset. The description now says so and names subscription, 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 by payerCoverage and rejected by isCharged, and only then is the description required to name it. Watched red: "the description must name subscription — the payer that is attributed and charged to nobody … expected false to be true".
    2. "unattributedBefore is calls predating the payer column" is the confident cause usage-coverage.ts:24-28 refuses. 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 by firstAttributedAt and 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 in usageFigures.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, byPayer and range were 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-33 argues correctly that the two NULL causes are indistinguishable per row and that collapsing them replaces one confident wrong number with another, and unmetered is 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 that payerCoverage sits at top level as a peer of the breakdowns rather than nested under the totals it 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; unauthenticated POST /mcp initialize returns 401 invalid_token, so I could not read it back off the wire. Verified instead: the deploy is green and smoke-tested, /health serves 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 next tools/list (hosts cache it — see #573).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdocumentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions