Turn the console's refusals into cards the user can act on - #46
Merged
Merged
Conversation
A 429 and a 402 from the console arrived as prose. The 429 rendered as a message
with a countdown and no way to see the ceiling it hit; the 402 rendered as a
sentence in a red box, telling the user their budget was gone and leaving them to
find the page that raises it.
Split by what actually helps, because the two are not the same kind of problem:
- 429 goes through `retryable()` and gets the existing retry card, linking to
the console's limits page. Retrying IS the remedy here, and `Retry-After`
already paces it.
- 402 does NOT. The console answers 402 rather than 429 specifically so that
clients stop -- its own comment says retrying an out-of-credit workspace
"just turns one refusal into six" -- and a card that reads "Retrying in 4s"
over something that will never succeed is worse than a plain message. These
go through a new `blocking()` instead.
So `SessionStatus` gains a `blocked` variant: the turn ended, no retry will fix
it, but there is something to click. It is distinct from `retry`, which means a
retry is IN FLIGHT and is drawn with a spinner and a countdown, and distinct from
a plain session error, which travels as message text and therefore cannot carry a
link at all -- which is why the user was told "budget exhausted" and left to go
looking.
The two 402s are told apart by `code`, and they need different pages: a key over
its cap is fixed on the key, an empty balance by topping up. `code` is the only
thing that distinguishes them on the OpenAI-compatible path, because the
console's fuller refusal body does not survive that envelope (fixed console-side
in the same change set).
Everything is gated on the provider being `redrob`. `rate_limit_exceeded` and
`insufficient_quota` are OpenAI's generic codes, so any vendor may send one, and
offering a link to our console for somebody else's rate limit would send the user
to a page that cannot help them.
This was referenced Sep 21, 2026
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 free
to 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.
Pairs with console #131, which keeps the refusal's
reasonalive on the OpenAI-compatible path so these two 402s can be told apart at all.Split by what actually helps
The 429 and the 402 are not the same kind of problem, so they do not get the same card.
retryable()/limitsblocking()/api-keysblocking()/billingA 402 must not be retried. The console answers 402 rather than 429 specifically so clients stop — its own comment says retrying an out-of-credit workspace "just turns one refusal into six". Routing it through
retryable()would both retry it and draw a spinner reading "Retrying in 4s" over something that will never succeed. That is worse than a plain message, so it would have been the wrong shortcut.A new
blockedsession statusThe turn ended, no retry will fix it, but there is something to click.
retry, which means a retry is in flight and is drawn with a spinner and countdown.Gated on the provider being ours
rate_limit_exceededandinsufficient_quotaare OpenAI's generic codes, so any vendor may send one. Handing the user a link to the Redrob console for somebody else's rate limit sends them to a page that cannot help. Tested both ways.Gates
The cowork side is a separate change in that repo: a
BlockedMessagecard reusing the retry card's action button, declared structurally because the published SDK does not carry the variant yet. Its typecheck adds 0 errors over baseline (11 pre-existingbun:testones, measured with everything stashed).