Skip to content

Turn the console's refusals into cards the user can act on - #46

Merged
its-janghoon merged 1 commit into
developfrom
feature/console-refusal-cards
Sep 21, 2026
Merged

its-janghoon merged 1 commit into
developfrom
feature/console-refusal-cards

Conversation

@its-janghoon

Copy link
Copy Markdown
Contributor

Pairs with console #131, which keeps the refusal's reason alive 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.

refusal path card links to
429 rate limit retryable() existing retry card /limits
402 key over budget blocking() new blocked card /api-keys
402 out of credit blocking() new blocked card /billing

A 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 blocked session status

The turn ended, no retry will fix it, but there is something to click.

  • Distinct from retry, which means a retry is in flight and is drawn with a spinner and countdown.
  • Distinct from a plain session error, which travels as message text and so cannot carry a link — which is exactly why the user was told "budget exhausted" and left to go find the page.

Gated on the provider being ours

rate_limit_exceeded and insufficient_quota are 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

typecheck        0 errors across the monorepo
packages/redrob  3380 pass / 0 fail   (retry.test.ts 68/68)
httpapi gate     pass=210 fail=0 skip=0 missing=0

The cowork side is a separate change in that repo: a BlockedMessage card 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-existing bun:test ones, measured with everything stashed).

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.
@its-janghoon
its-janghoon merged commit eb0389d into develop Sep 21, 2026
26 of 27 checks passed
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.

1 participant