Repository navigation
Description
Two declared types are narrower than the values they describe — found by the first typecheck of worker code
6d44ac7 (#599) put worker sources and tests under tsc for the first time. Two production type defects surfaced and were deliberately left for their own ticket.
1. UsageSummary["totals"] omits fields aggregateUsage returns
workers/api/src/lib/usage.ts:319 — the declared totals shape lacks cacheReadTokens and cacheWriteTokens, which aggregateUsage computes and returns.
The console already works around it: store/console/src/pages/Usage.tsx:35 declares its own parallel shape with those fields optional. So the data crosses the wire, the UI reads it, and the type says it does not exist — which means the compiler cannot help anyone who touches either side.
This is the same drop-on-the-way-out shape #547 fixed for daily. Second instance, same file.
2. JsonSchema.properties[].type is a scalar where the format uses an array
workers/api/src/lib/connectors/types.ts:111 declares type as string. JSON Schema draft-07 permits string[], and behaviourToolSchema uses the array form. So a legal schema this codebase itself produces does not satisfy the type meant to describe it.
Why they belong together
Both are the same defect: a declared type narrower than the value it describes, surviving because nothing compiled the code that would have caught it. Both were invisible for the same reason and became visible on the same day, which is the argument for one guard rather than two fixes.
It is also the type-system instance of the pattern this batch has been removing everywhere else — a stored or declared fact that the runtime contradicts (#587 runtime.status, #588 RunHealth, #592 attempts, #596 waiting_until). Here the contradiction is between a type and its own producer.
Acceptance criteria
UsageSummary["totals"]carries every fieldaggregateUsagereturns, andUsage.tsxconsumes that type instead of a parallel declaration. The parallel shape is the tell — remove it, do not sync it.JsonSchema.properties[].typeacceptsstring | string[], andbehaviourToolSchematypechecks against it without a cast.- A test asserts the declared return type matches what the function actually returns for
aggregateUsage— a compile-time assertion is now genuinely enforced after Three tsconfigs exclude test files and CI typechecks none of them, so a type-level guard in any worker test is inert #599, so this is the first issue that can rely on one. - Check whether other API response types have a parallel console-side declaration. Two instances of one shape (The Usage chart shows 4.2M tokens for the day the 250M ceiling was tripped — the daily series drops the cache columns, which are 98.2% of what the ceiling counts #547, this) suggests a habit; report the count with its denominator per ADR 0002 rather than fixing only the two known.
Verified vs inferred
- Verified: both
file:linecitations; that the console declares a parallel shape atUsage.tsx:35; thatbehaviourToolSchemauses the array form; that The Usage chart shows 4.2M tokens for the day the 250M ceiling was tripped — the daily series drops the cache columns, which are 98.2% of what the ceiling counts #547 fixed the same shape fordaily. - Inferred: that other response types share the parallel-declaration habit. AC4 exists to measure it — it was not measured, and if the answer is "just these two", say so and close that arm rather than manufacturing work.
Activity
fix(types): the usage response is declared once and imported by the p…
fix(usage): a breakdown row says how many CODING SESSIONS are behind …
serge-ivo commentedon Aug 15, 2026
Shipped in
c93c4ddandf6f1a54. Both defects fixed, AC4 measured, and the measurement is much larger than the issue supposed.1 —
UsageSummary["totals"](AC1, AC3)lib/usage-shape.tsis now the single declaration of theGET /v1/usagebody.store/console/src/pages/Usage.tsxtype-imports it and its four parallel declarations (Bucket,Day,Unmetered,UsageData) are gone — removed, not synced, as the issue asked.usageFigures.ts'sPayerCoveragewas a third copy and went the same way.The import is
import typeand esbuild erases it, so it adds no bundle byte and no runtime edge. The direction matters: the reverse edge (the API Worker depending on the published@proagentstore/sdk) stays refused for the reasonslib/normalize-speech.tsrecords — a new exports subpath and a build step in front ofwrangler deploy— and none of those apply to a type the compiler deletes.AC3, two guards, because either alone is passable:
aggregateUsage's accumulator is now ANNOTATED (const totals: UsageTotals). A column summed and not declared — the actual UsageSummary omits cache-token fields the aggregator returns, and JsonSchema.type is a scalar where the format uses an array #608 defect — or declared and not summed, stops compiling in the file that does the summing. Verified by reverting the type and watchingtscfail on three lines.usage-shape.test.tsreads the route's ownreturn c.json({…})literal and asserts its keys equal the declared type's, sorange/unmeteredare covered too.The route deliberately keeps that literal rather than an annotated
const body:workers/mcp/src/instance-tools/account.test.tsparses the same line, and annotating the response broke it. Two tests now read that one line; it is commented as load-bearing.The tell that this was overdue is that the test file had already written the workaround and dated it —
cacheTotals()existed only to read fields the type denied, with a docstring saying "once the production type carries them this helper can be deleted". Deleted.2 —
JsonSchema.properties[].type(AC2), and the live bug it was hidingWidened to
string | string[];behaviourToolSchemanow returnsJsonSchemaandtool-registry.ts'sas ToolDef["jsonSchema"]cast is gone, as is the double-unknowninbehaviour-tools.test.ts.Widening it surfaced a real defect.
routes/tools.ts'smatchesTypeswitched ontypeas a string, so an ARRAY fell through todefault: return true— every property declared with the list form was accepted whatever was passed.behaviourToolSchemaemits["number","null"]on everyset_behaviourfield, so the tool-input validator did nothing at all for the tool most likely to be called with a value a model invented, and it looked exactly like a validator that worked.Measured:
POST /v1/instances/:id/tools/set_behaviour {"technicality":"banana"}returned 200. It now returns 400 withField "technicality" must be a number or null. The test fails on the old code — watched.The validator moved to
lib/json-schema-validate.tson the way past, and now has a direct unit suite that runs against the REAL generatedset_behaviourschema rather than a paraphrase of it. Two reasons: it is a pure rule about a data format that was reachable only through a Hono app with a D1 double, androutes/tools.tshad zero headroom under the #302 ratchet — extracting it is the better test surface and what keeps the pin honest without raising it.AC4 — measured, and the answer is not "just these two"
90 of 125 console-side API-response types are parallel declarations of a Worker shape — 72%.
api<T>(…)understore/console/src, across 162 call sites, plus the named types they transitively reference to describe response data.workers/api/srccarries its own declaration of the same shape. The remaining ~35 are not parallel because the Worker builds the body inline with no named type at all.workers/api/src(surfaces.test.ts:71,toolLiterals.test.ts:29) and both guard string-literal vocabularies, not field sets.api<T>is an unchecked cast —packages/sdk/src/client.ts:157isreturn data as T.Mirrors <worker type>comments plusstats-types.ts:2declaring all nine of its types mirrored.The two known instances are neither representative nor exceptional — they are the harder subclass.
UsageSummary.totalsand.dailywere anonymous object literals Worker-side, so there was no name to diff against, which is why no guard was possible before this change. Meanwhile three of their neighbours in the same file (Bucket ↔ UsageBucket,Unmetered ↔ UnmeteredUsageSummary,UsageData ↔ UsageSummary) were ordinary named parallels the issue never mentions. All five are now one declaration.At least 15 pairs disagree with the Worker today. The worst are wrong field names on both-sides-optional fields, where the compiler is silent by construction:
KnowledgeDoc.createdAtaddedAtIndexingTab.tsx:188Credential.history?types.ts:211CredentialSummary.recoveryHistory?lib/credentials.ts:24hasPassword/hasPin/hasRecoveryCodes/lastUsedAtTranslationConfigWirelib/use-gloss.ts:22TranslationConfigroutes/instances-translation.ts:53transliterate: booleannot declared console-side at allProfileFieldProfile.tsx:9ProfileFieldDeflib/profile.ts:6private: booleanrequired, consoleprivate?— a privacy flag silently readingundefinedSearchHitVectorsSection.tsx:30VectorSearchResultagent-storage-types.ts:40idabsent console-sideConnectionlib/teamwork.ts:25ConnectionViewlib/connections.ts:62sourceInstanceIdabsent console-sideAgent/CatalogAgentAgentRowroutes/agents.ts:13routes/agents.ts:233-237) selectscreator_login,creator_name,creator_avatar,store_type,subscriber_count, none of whichAgentRowdeclaresI did not fix the other 88. Ninety rewrites is not this ticket, and several are genuine judgement calls (an optional field guarding an older API is sometimes right). What this change does establish is the shape that generalises: one Worker-owned declaration in a leaf module with no
Envimport, type-imported by the console.Recommend filing two follow-ups rather than one omnibus — say the word and I will have them written:
KnowledgeDoc.createdAt,Credential.history) since those are silently readingundefinedin production today;AgentRowunder-declaring its own route's SELECT, which is a Worker-side defect the console merely inherits.Verified
pnpm typecheck(11 projects), 9,156 unit tests (526 files), biome with--error-on-warnings, all six script gates includingcheck-file-size,check-migrations --require-history,docs:drift, bothtsconfig.test.jsongates (#599), consoletsc --noEmitandvite build. CI green onf6f1a54; API and Host workers deployed.One fix-forward, my error:
c93c4ddwent red on the #599 worker-test typecheck —usage-shape.test.tsusedfileURLToPath(new URL(…)), which does not compile underworkers/api/tsconfig.test.jsonbecause the DOMURLand node's are different types there. I ran that gate before adding the file and not after. Fixed inf6f1a54using the formaccount.test.tsalready uses.fix(usage): the session note reports two counts instead of their sum,…
serge-ivo commentedon Aug 18, 2026
Audit verdict — DELIVERED, all four criteria. Closing.
Re-read against
maintoday rather than taking the shipping comment at face value.AC1 — one declaration, consumed by the console.
workers/api/src/lib/usage-shape.tsexists and is the single owner of theGET /v1/usagebody.store/console/src/pages/Usage.tsx:25isimport type { UnmeteredUsageSummary, UsageBucket, UsageDay, UsageResponse } from ".../workers/api/src/lib/usage-shape"— a type-import, so esbuild erases it. The four parallel console declarations are gone, not synced, as the issue asked.AC2 —
typeaccepts the array form.workers/api/src/lib/connectors/types.ts:120:with the reason recorded at
:111.AC3 — the compile-time assertion exists. The
aggregateUsageaccumulator is annotated, so a column summed-but-undeclared fails in the file that sums it;usage-shape.test.tsadditionally reads the route'sreturn c.json({…})literal and diffs its keys against the declared type.AC4 — measured, and it exceeded the issue's supposition. 90 of 125 (72%), 0 drift guards, ≥15 pairs already disagreeing. The issue asked for the number rather than the fix, and the number was produced. It is now #616, which carries the inventory and the worst-first list — so the arm is tracked, not dropped.
One thing worth naming, because it is the best argument the issue made and it was borne out
Widening
typetostring | string[]surfaced a live security-adjacent defect the issue did not predict:matchesTypeswitched ontypeas a string, so the array form fell todefault: return true.behaviourToolSchemaemits["number","null"]on everyset_behaviourfield, so the validator for the tool most likely to be called with a model-invented value validated nothing.POST /v1/instances/:id/tools/set_behaviour {"technicality":"banana"}returned 200 and now returns 400. That is the payoff for "a declared type narrower than the value it describes" being treated as a real class rather than two typos.Stale citations, recorded for the citation-decay measure
All three of this issue's
file:linecitations no longer resolve to what they claimed — because the fix moved them, which is the benign case:workers/api/src/lib/usage.ts:319→ nowusageDay(); the shape moved tolib/usage-shape.ts.store/console/src/pages/Usage.tsx:35→ now unrelated copy; the parallel declaration is gone.workers/api/src/lib/connectors/types.ts:111→ now the doc comment explaining the widened type; the declaration is at:120.