Skip to content

fix(frontend): only offer Retry for failures resending the prompt can fix - #263

Merged
dovvnloading merged 1 commit into
mainfrom
fix/retry-only-where-it-helps
Sep 7, 2026
Merged

fix(frontend): only offer Retry for failures resending the prompt can fix#263
dovvnloading merged 1 commit into
mainfrom
fix/retry-only-where-it-helps

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

The problem

The composer banner renders one "Retry last message" button for every failure it can show — and most of those are not generation failures. Clicking it after an unrelated error resends a prompt the thread has already answered, adding a duplicate turn.

The clearest case: fork a message, the fork fails, the banner says "Could not fork this chat." — and offers to resend your last prompt.

Root cause

onRetry was gated only on whether a prompt had ever been sent:

onRetry={lastPrompt ? retryLastPrompt : undefined}

Six code paths raise this banner. Three of them cannot be fixed by resending:

Site Why Retry is wrong
failed fork not a generation at all; the thread is answered and unchanged
failed stop the response is still running — a resend starts a second one alongside it
reload failed after generation finished the answer exists and was saved; only the refetch failed

And retryLastPrompt makes it concrete. With no dangling user turn — the normal state of an answered thread — it falls through to the ambiguous-admission branch and calls startGeneration(lastPrompt, ...), submitting the old prompt as a new message.

The other three sites are genuine and keep Retry: a generation that failed, a runtime that was unavailable, and an admission that could not be started. The replay/idempotency logic behind them is untouched.

The fix

ScopedError records whether resending is the remedy, and the button renders only for an error that says so:

onRetry={lastPrompt && visibleError?.retryable ? retryLastPrompt : undefined}

Each of the six call sites now states its own answer at the point where it knows it, rather than the banner guessing.

A note on the test

The regression test sends and completes a turn before forking, deliberately.

My first version simply loaded a thread with existing messages and forked. It passed against the unfixed code — because lastPrompt is only set inside startGeneration, so a thread that was merely loaded never renders a Retry button at all, and queryByRole(...).not.toBeInTheDocument() was trivially satisfied. The test now establishes lastPrompt first, so the button genuinely would appear without the fix.

Against the unfixed code it fails; the three existing tests that exercise Retry on a real generation failure pass throughout, which is the half that must not regress.

Check Result
npm test -- --run 273 passed, 31 files
npm run typecheck clean
npm run lint clean
python -m pytest -q 907 passed

Compatibility and rollback

One component's local state and one render condition. No API contract, stored data, or migration. Every error still displays and still dismisses; only the Retry affordance is withheld where it could not have helped. Reverting the commit restores the previous behaviour exactly.

Limits

One test covers the fork path. The stop-failure and reload-failure sites take the same single mechanism and are not separately covered — worth adding if either grows more logic, but three near-identical integration tests for one boolean seemed like poor value.

A failed fork now offers no action at all beyond dismissing the banner. Retrying the fork would be the useful button there, and that is a feature rather than part of this fix.

🤖 Generated with Claude Code

… fix

The composer banner shows one "Retry last message" button for every failure it
can display, and most of those are not generation failures. Clicking it after
an unrelated error resent a prompt the thread had already answered, adding a
duplicate turn.

Three of the six sites that raise this banner cannot be fixed by resending:

- a failed fork -- not a generation at all; the thread is answered and
  unchanged;
- a failed stop -- the response is still running, so a resend starts a second
  one alongside it;
- a reload that failed after generation finished -- the answer exists and was
  saved, only the refetch failed.

The other three are genuine: a generation that failed, a runtime that was
unavailable, and an admission that could not be started. Those keep Retry, and
the replay logic behind it is untouched.

`ScopedError` now records whether resending is the remedy, and the button is
rendered only for an error that says so.

The regression test sends and completes a turn before forking, on purpose:
`lastPrompt` is only set once something has been sent this session, so a test
that forks in a freshly loaded thread never renders a Retry button and passes
against the unfixed code. My first version did exactly that.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dovvnloading
dovvnloading merged commit 922d0e3 into main Sep 7, 2026
7 checks passed
@dovvnloading
dovvnloading deleted the fix/retry-only-where-it-helps branch September 7, 2026 15:57
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