Uh oh!
There was an error while loading. Please reload this page.
fix(chatbot): accept the landed AI quota ledger vocabulary (four dialects) - #6383
Merged
Merged
Conversation
`AI_QUOTA_CODES` / `AiQuotaCode` held only the three lowercase legacy literals while cloud PR #1238 landed the SCREAMING_SNAKE ledger vocabulary, so `asAiQuotaCode`'s exact `Set.has` missed every converged producer and a quota-exhausted user fell through to the generic "Response failed" banner instead of the upgrade / top-up CTA. - Add `AI_ALLOWANCE_EXHAUSTED`, `AI_DESIGN_QUOTA_EXHAUSTED` and `AI_DATA_CHAT_TRIAL_EXHAUSTED`, KEEPING the lowercase trio for transition-period producers. - Read the companion fields from the declared envelope's `error.details` (the position cloud PR #1238 shipped) with the legacy top-level read as the fallback limb, matching the total order the code lookup already uses. - Add `resetsTonight`, set only when a producer sends an actual boolean. - Widen the dialect matrix from three to four and pin that the per-turn cap's generic `QUOTA_EXCEEDED` keeps its rate-limit path. No existing assertion in `tool-display.test.ts` was weakened or removed.
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-support-ai
marked this pull request as ready for review
August 25, 2026 19:11
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#3804
All readings below were taken at
7e8272312(this branch's head).What was shipping
AI_QUOTA_CODES/AiQuotaCodeinpackages/plugin-chatbot/src/tool-display.tsheld only the three lowercase legacy literals, andasAiQuotaCodeis an exactSet.has. cloud PR #1238 landed the SCREAMING_SNAKE ledger vocabulary on the producer side, so every converged 429 missed the quota branch and a quota-exhausted user got the generic red "Response failed" banner instead of the upgrade / top-up CTA.Changes
AI_ALLOWANCE_EXHAUSTED,AI_DESIGN_QUOTA_EXHAUSTED,AI_DATA_CHAT_TRIAL_EXHAUSTED— keeping the lowercase trio, which transition-period producers still emit.error.details.messageEn/upgrade/topUpnow read from the declared envelope'serror.details(the position cloud PR Create independent server app for Vercel deployment #1238 shipped) with the legacy top-level read as the fallback limb. The declared position wins, matching the total order the code lookup already used. Non-boolean flag values are ignored exactly as the old=== trueread ignored them.resetsTonightadded toAiQuotaError, set only when a producer sends an actual boolean. The field's position is measured; its type is not pinned by anything readable from this repo, so an otherwise-typed value staysundefinedrather than being coerced to afalseno producer declared.Test readings — ghost-assertion guard (both directions)
The fix file was reverted to unmodified
origin/mainwhile the new tests stayed in place, then restored.tool-display.tsatorigin/main, new tests presentpnpm exec vitest run packages/plugin-chatbot/src/tool-display.test.tsTest Files 1 failed (1)·Tests 16 failed | 33 passed (49)· exit 1Test Files 1 passed (1)·Tests 49 passed (49)· exit 0Mutation proven on disk before measuring, not inferred from an exit code: uppercase-code occurrences in the file went
6 → 0andnested.detailsreads1 → 0; the mutated blob hash equalled theorigin/mainblob and differed from the HEAD blob.Restore proven on disk: restored blob
652041b63c16cad90f8ac4342431747d83a544c9matched the HEAD blob,git diff HEAD --statwas empty, and the uppercase count was back to6.tool-display.tsest.ts) and exited 1 withNo test files found. That is a zero-match exit, not a red gate — it was discarded and the ablation was re-run with the correct path. The 16-failure reading above is the real one.The 16 failing assertions were exactly the new-behavior ones
it.each(LEDGER_CODES)— three uppercase codes across each of the three pre-existing dialects (flat guardrail, service-ai sibling key, declared envelope).declared envelope + ledger vocabulary — companions in error.detailsdescribe.Degenerate-control guard
tool-display.test.tsalready exercised the lowercase trio heavily, so a lowercase-only case proves nothing — it passes against the unfixed code. The assertions that actually pin this change are:LEDGER_CODESconstant (the three uppercase literals).declared envelope + ledger vocabularydescribe — including itserror.detailsreads, which fail againstorigin/maineven with a lowercase code, becauseerror.detailswas not read at all before.Both groups are called out in an in-file
DEGENERATE-CONTROL NOTEcomment so the next reader does not have to re-derive it.No existing assertion weakened or deleted
git diff -U0 <base> -- packages/plugin-chatbot/src/tool-display.test.ts | grep '^-'returns six lines, all comments — zeroit(and zeroexpect(lines removed. The two pre-existingQUOTA_EXCEEDEDassertions (misses on a declared non-quota code,falls through to a legacy limb when the nested code is unrecognized) are untouched and still green. One stale comment was refreshed: it said theai_quota_exhaustedgap was "cloud#1168's to align", which has now resolved — the assertion itself is unchanged and still correct, sinceai_quota_exhaustedis in neither vocabulary.The dispatch order said to accept three vocabularies, including generic
QUOTA_EXCEEDED, inparseAiQuotaError. That one item is not implemented here, on measured grounds. Everything else in the order is.What the consumer actually does today (
packages/plugin-chatbot/src/ChatbotEnhanced.tsx):sendAwareFetchtags every non-2xx withnotSent: trueandstatus(useObjectChat.ts).SendErrorNoticewhenisUnsentSendError(error) && !parseAiQuotaError(error).isRateLimitErrorsees status 429 and the user gets "You're sending messages too quickly. Your message is kept below — wait a moment and try again." with the typed text restored. Correct copy for a cap that resets in a minute.What adding
QUOTA_EXCEEDEDto the set would do:parseAiQuotaErrorreturns truthy, the predicate flips, andErrorBannerrenders instead — title "Upgrade needed" (unconditional,ChatbotEnhanced.tsx:4026) plus, when the host wiresonUpgrade, an "Upgrade plan" button.packages/app-shell/src/console/ai/AiChatPage.tsx:2182wires exactly that, towindow.open(cloudPricingDeepLink()). A user who hit the per-turn message cap would be sent to the pricing page.That contradicts the 2026-08-11 Option A rationale it was meant to serve — the three
AI_*codes were admitted to the closed ledger because they carry distinct CTAs, while the per-turn cap deliberately stayed generic for having none.The cross-seat relay's premise appears to be inaccurate about this repo. It warned against
parseAiQuotaError"dropping itsQUOTA_EXCEEDEDbranch".origin/main'sparseAiQuotaErrornever had one — it explicitly assertednullforQUOTA_EXCEEDED. The handling the relay wanted preserved lives one branch along, inisUnsentSendError/isRateLimitError. This is worth routing back to therepo:cloudseat.What this PR does instead: pins that routing where it actually lives, so the split the relay was worried about cannot close silently. Three assertions in
generic QUOTA_EXCEEDED (per-turn cap) keeps the rate-limit pathassertparseAiQuotaErrormisses it, thatisUnsentSendError+isRateLimitErrorboth hold, and that a non-quota 429 (RATE_LIMIT_EXCEEDED) still falls through to the generic path.origin/maintoo — they are regression pins for preserved behavior, not ghost-guarded new-behavior assertions, and are labelled as such in the file. If the PM/maintainer rules the other way, it is a one-line change (add the literal toAI_QUOTA_CODES) plus inverting those pins.Gates run locally
pnpm exec vitest run packages/plugin-chatbot/src/tool-display.test.tsTests 49 passed (49)· exit 0pnpm --filter @object-ui/plugin-chatbot type-checkpnpm --filter @object-ui/plugin-chatbot lint✖ 88 problems (0 errors, 88 warnings)· exit 0node scripts/check-changeset-presence.mjs✅ 2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 5248 tracked text file(s); skipped 85 binary)node scripts/check-vi-mock-specifiers.mjs✅ check-vi-mock-specifiers: OKNotes on the two readings that needed care:
type-checkfirst came back exit 2 with a wall ofTS2307: Cannot find module '@object-ui/components'. That is the unbuilt-dependency signature in a fresh worktree, not a red gate — this package'stsconfig.jsonreplaces the rootpaths, so@object-ui/*resolves todist/*.d.ts. Afterpnpm --workspace-concurrency=2 --filter '@object-ui/plugin-chatbot^...' build(exit 0),type-checkis exit 0.type-check.tsconfig.jsonexcludes**/*.test.ts, so that half says nothing about it;tsc -p tsconfig.test.json --listFileswas checked and returns 1 hit forsrc/tool-display.test.ts.The lint warnings are pre-existing and in unrelated files (
usePendingActions.tsand friends); the gate is0 errors.Repo-wide scans (
pnpm lintacross the workspace, thecheck:*farm) are CI's run and were not duplicated locally.Not touched
content/docs/releases/— the release-notes input here is the changeset (.changeset/ai-quota-ledger-vocabulary-3804.md,patchon@object-ui/plugin-chatbot).The
cloudrepo is outside this session's scope; the producer reading used is the 2026-08-12 cross-seat relay recorded on the card, not a guess at the producer's shape.⛔ Left as draft deliberately — the PM lands this. Not self-merged, not marked ready.
Generated by Claude Code
Generated by Claude Code