Skip to content

feat: real recovery UI for generation failures - #55

Merged
crewcricle merged 1 commit into
mainfrom
feat/generation-failure-recovery
Aug 8, 2026
Merged

feat: real recovery UI for generation failures#55
crewcricle merged 1 commit into
mainfrom
feat/generation-failure-recovery

Conversation

@rprabhat

@rprabhat rprabhat commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Item #4 from the consolidated fix plan - the highest-severity item, deliberately scoped as its own PR given its size relative to #1/#2/#3.

Root cause (accountant audit round three - David hit "Query failed - please try again" three times in a row on an identical retry, with the backend's own /health reporting healthy throughout): an unhandled exception inside the SSE generate() async generator in /query/stream just crashed it. FastAPI closed the connection with no event; the frontend's EventSource.onerror fired with zero information, and the dashboard showed a generic message - no distinction between "provider is briefly at capacity" (worth retrying) and a real bug, and no retry action beyond a one-line toast.

Backend (query.py): the astream loop is wrapped in try/except. On failure:

  • The query row is marked failed (mirroring the existing embed-failure handling just above it).
  • A typed {"type": "error", "transient": bool, "message": str} SSE event is emitted, followed by a normal [DONE] - so EventSource resolves cleanly instead of erroring.
  • transient is classified via isinstance(e, openai.APIError) - litellm normalises every provider's rate-limit/timeout/connection/5xx failures onto openai's exception hierarchy, a real checkable signal instead of guessing from message text.

Frontend (query/page.tsx): a new generationError state renders an actual card (message + context-appropriate note + a "Try again" button that re-asks the same question), instead of collapsing into the generic error string other unrelated flows (save/edit/promote) share. The pre-existing outer catch (a dropped connection before any typed event arrives) now populates the same state, so the retry action is available for that failure mode too.

Verification

  • Backend: 844 tests pass (2 new - a transient provider error emitting a retryable typed event with the query row marked failed and partial tokens preserved, and a non-provider exception emitting a non-transient event). Both exercise the real StreamingResponse.body_iterator at the ASGI level, not a mocked-away version of the new code.
  • tsc --noEmit and eslint clean.
  • Live UI verification not possible in this session: the local dev DB tunnel isn't reachable in this sandbox (same limitation hit earlier this session for other items), so a full authenticated end-to-end browser check couldn't be run. Confidence rests on the ASGI-level backend tests (which exercise real SSE byte output) and reviewed/type-checked frontend wiring - flagging this explicitly rather than claiming a live check that didn't happen.

Reviewer notes

  • Worth a real click-through once deployed to confirm the retry card renders and the "Try again" button re-asks cleanly - the one piece this PR couldn't verify live.

🤖 Generated with Claude Code

Item #4 from the consolidated fix plan. Root cause (accountant audit
round three, David's Division 820 question - three identical retries,
all "Query failed - please try again", /health reporting fine
throughout): an unhandled exception inside the SSE generate() loop
just crashed the async generator. FastAPI closed the connection with
no event, EventSource fired a bare onerror, and the dashboard showed a
generic message with zero distinction between "the AI provider is
briefly at capacity" and a real bug - and no retry action, just a
one-line toast.

Backend: the astream loop is now wrapped in a try/except. On failure,
the query row is marked failed (mirroring the existing embed-failure
handling), and a typed `error` SSE event is emitted before a normal
[DONE] - so EventSource resolves cleanly instead of erroring. Classifies
transient vs not via litellm's exception hierarchy: every provider's
rate-limit/timeout/connection/5xx failures normalise onto openai's
exception classes (RateLimitError, Timeout, APIConnectionError, etc.
all subclass openai.APIError), a real checkable signal rather than
guessing from message text.

Frontend: a new `generationError` state (message, transient, the
question that failed) renders an actual card with a "Try again" button
that re-asks the same question, instead of collapsing into the
generic error string every other unrelated save/edit/promote flow
also uses. The pre-existing outer catch (a dropped connection before
any typed event arrives) now populates the same state instead of a
bare toast, so the retry action is available for that failure mode
too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
taxflow-dashboard Ready Ready Preview Aug 8, 2026 8:38am

@crewcricle
crewcricle merged commit 813f2e5 into main Aug 8, 2026
7 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.

2 participants