Skip to content

[Billing Credits] PR-I4: Wallet-locked + concurrency error states#1992

Closed
Vu-John wants to merge 4 commits intomainfrom
feat/billing-credits-error-states-i4
Closed

[Billing Credits] PR-I4: Wallet-locked + concurrency error states#1992
Vu-John wants to merge 4 commits intomainfrom
feat/billing-credits-error-states-i4

Conversation

@Vu-John
Copy link
Copy Markdown
Collaborator

@Vu-John Vu-John commented May 1, 2026

[Billing Credits] PR-I4: Wallet-locked + concurrency error states

The server now returns two new error states the inspector currently renders
generically. This PR threads the new fields through the chat error parser
and renders distinct UX for each, plus tightens one top-up dialog edge case.

What's new

1. Wallet-locked banner — when the server marks an account as paused
(walletLocked: true), the chat error renders a dedicated "Account under
review" banner with a contact-support link. No top-up button, no retry —
the user can't self-serve out of this state.

┌─ Account under review ───────────────────────────────────┐
│ ⛨  We've paused this account while a recent payment is   │
│    reviewed. Reach out to support to get back in.        │
│                                          [Reset chat]    │
└──────────────────────────────────────────────────────────┘

2. Concurrency-throttle retry UI — when the server returns a
rate-limit with limitKind: "concurrency", the chat error renders a
transient banner with a small Retry button and a seconds-level
countdown. No top-up CTA (topping up doesn't help — the user just needs
to wait).

┌─ Another credit-funded chat is finishing. Retry in 8 seconds. [Retry]

3. Existing rate-limit + canTopUp path — unchanged. Daily quota
exhausted with a paid wallet option still shows the existing
"Top up to keep chatting" CTA.

Implementation

  • chat-helpers.ts: FormattedError gains three optional fields
    (walletLocked?: boolean, limitKind?: "total" | "concurrency",
    retryAfterMs?: number). Both parser branches (rate-limit and
    generic-structured) defensively extract them. retryAfterMs is
    emitted only for the concurrency case so the existing toEqual
    assertions on rate-limit results stay tight.
  • error.tsx: two new early-return branches at the top of the
    component for the locked and concurrency states. The existing
    rate-limit / generic / auth-error paths are unchanged.
  • ChatTabV2.tsx: a new handleRetryConcurrencyMessage callback
    reuses the existing lastSentUserMessageRef to resubmit the user's
    last typed message. The onRetry prop is gated on the error being a
    concurrency throttle so unrelated retryable errors don't surface a
    Retry button.
  • useCreditTopup.ts: the startCheckout catch block now detects
    the server's "Too many top-up attempts" message and re-throws a
    sanitized "You've hit the top-up rate limit. Try again in a few
    minutes." The dialog's existing toast picks up the new message, so
    no double-toast and no unhandled propagation.

Tests

Added 3 parser tests in chat-helpers-clone.test.ts:

  • Wallet-locked field passes through.
  • Concurrency limitKind + retryAfterMs pass through together.
  • Legacy 429 without either field — both undefined, no crash.

The one existing assertion that turned out to be sensitive to the new
limitKind: "total" field (the JSON included it but the assertion
didn't) was extended to expect it.

error.tsx has no existing render-test pattern, so per the spec the
new states get manual verification rather than fabricated tests.

Verification (manual)

  • Craft a 429 with walletLocked: true → "Account under review"
    banner, no top-up, mailto link present.
  • Craft a 429 with limitKind: "concurrency" and retryAfter: 8000
    transient banner with "Retry in 8 seconds" + Retry button. Click
    Retry → resubmits the last typed user message.
  • Existing daily-limit + canTopUp path → unchanged.
  • Simulate createCreditCheckoutSession rejecting with "Too many
    top-up attempts. Try again in 5 minutes." → toast surfaces the
    friendlier sanitized copy; dialog doesn't crash.

Local run of all touched suites green:

  • chat-helpers-clone.test.ts — 12 tests
  • client/src/components/billing — 23 tests across 5 files
  • client/src/hooks/__tests__/useCreditTopup* — 19 tests across 2 files
  • client/src/components/chat-v2/** and OrganizationsTab.billing.test.tsx — 444 tests across 38 files

Stack

This PR is a logical follow-up to PR-I3 (the existing billing-credits
stack), but submitted independently against main rather than stacked.
It builds on the FormattedError infrastructure introduced in PR-I1, so
expect to rebase once the I-stack merges.

Rebased 2026-05-01 onto the redesigned PR-I3 tip — the I3 design
simplification (drop dollar values from the bars, retire the activity
table, switch to hasPurchaseHistory boolean) does not interact with
this PR's scope. Diff cleanly reapplied with no conflicts.

Risk

Low. All new fields are optional; missing or wrong-type values default
to undefined (no crash, no false positives). The new UI states are
gated on explicit fields, so legacy responses keep their existing
rendering. The dialog's existing error path remains the same — only the
message it surfaces is now sanitized.

@Vu-John Vu-John temporarily deployed to preview-pr-1992 May 1, 2026 08:58 — with GitHub Actions Inactive
@chelojimenez
Copy link
Copy Markdown
Contributor

chelojimenez commented May 1, 2026

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 1, 2026

Internal preview

Preview URL: https://mcp-inspector-pr-1992.up.railway.app
Deployed commit: 95a7916
PR head commit: b465315
Backend target: staging fallback.
Health: ❌ Convex unreachable — see upsert-preview job logs (staging may need convex deploy)
Access is employee-only in non-production environments.

Vu-John and others added 4 commits May 1, 2026 17:00
Threads two new error states from the server through the chat error
parser and renders distinct UX for each, plus tightens a top-up dialog
edge case.

- FormattedError gains optional walletLocked, limitKind, and
  retryAfterMs fields.
- Chat error banner now renders three states in priority order:
  walletLocked → "Account under review" with a contact-support link;
  limitKind="concurrency" → transient retry banner with seconds-level
  countdown and a Retry button; otherwise the existing rate-limit /
  generic rendering is unchanged.
- ChatTabV2 surfaces the concurrency Retry handler only when the error
  is concurrency-classified, so unrelated retryable errors don't grow
  a Retry button.
- useCreditTopup re-shapes the server's "Too many top-up attempts"
  error into a friendlier user message before re-throwing, so the
  dialog's existing toast picks it up cleanly.

Test coverage extends the parser tests for the two new fields and the
legacy back-compat case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Vu-John
Copy link
Copy Markdown
Collaborator Author

Vu-John commented May 2, 2026

Superseded by #1995 — same content, repackaged as a Graphite-tracked PR stacked on PR-I3 (#1990) so the inspector half of the billing-credits work stays unified in one stack. Closing.

@Vu-John Vu-John closed this May 2, 2026
@Vu-John Vu-John deleted the feat/billing-credits-error-states-i4 branch May 2, 2026 01:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants