RFC: Per-step retry & error UX in classroom generation preview
- Dominant language
- TypeScript
- Stars
- 37.2k
- Forks
- 5.9k
- Avg merge
- 1d 3h
- Merged PRs (30d)
- 195
Description
## Summary
Classroom generation preview pipeline (`app/generation-preview/page.tsx`) currently has weak error handling: only outline step has auto-retry, all other failures hard-abort the entire pipeline, and two steps fail silently. Propose unified retry + per-step error UI so transient failures self-heal and users can recover without restarting the whole flow.
## Current State
Pipeline runs 7 sequential steps in `startGeneration()` (page.tsx:215-971). Each step's failure behavior today:
| Step | Auto-retry | On failure | User feedback |
|---|---|---|---|
| PDF parse (244-292) | none | abort all | red `AlertCircle` + text |
| Web search (385-427) | none | **silent skip, continue** | none |
| Outline SSE (route.ts:285-396) | **server-side, 2 retries** | abort all | `outlineRetrying` status text |
| Agent profiles (664-753) | none | **silent fallback to preset** | none |
| Slide content (808-833) | none | abort all | red error icon |
| Actions (840-865) | none | abort all | red error icon |
| TTS (905-936) | none | abort all | red error icon |
Sole recovery action: "Go Back and Retry" button (page.tsx:1406) — restarts entire pipeline from step 0.
`AbortError` is swallowed silently (page.tsx:965-967).
## Problems
1. **No per-step retry** — one failed image gen wastes everything before it.
2. **Inconsistent retry policy** — only outline has it; rest don't.
3. **Silent failures** — web-search and agent-profile fallbacks leave user unaware that output quality degraded.
4. **Weak retry indication** — outline retry only mutates `statusMessage`; no attempt counter, no toast.
5. **All-or-nothing error model** — single `error` string + `currentStepIndex` cannot represent "step 3 failed, steps 1-2 cached".
## Proposal
### A. Shared retry wrapper (client)
`lib/generation/retry.ts`:
```ts
type RetryOpts = {
max: number; // default 2
stepKey: StepKey;
signal?: AbortSignal;
onAttempt?: (attempt: number, err: Error) => void;
};
export async function withRetry(
fn: (attempt: number) => Promise,
opts: RetryOpts,
): Promise;
```
Exponential backoff (300ms / 800ms / 2000ms). Respects `AbortSignal`. Classifies errors: network/5xx/timeout retryable; 4xx (except 429) non-retryable.
Wrap each `fetch` call in the pipeline.
### B. State machine
Extend `app/generation-preview/types.ts`:
```ts
type StepStatus = 'idle' | 'running' | 'retrying' | 'failed' | 'done' | 'skipped';
type StepState = {
status: StepStatus;
attempt: number;
maxAttempts: number;
lastError?: { message: string; code?: string };
startedAt?: number;
finishedAt?: number;
};
type PreviewState = {
steps: Record;
// ...existing
};
```
Replace single `error: string` + `currentStepIndex: number` with `steps` map.
### C. UI changes
- **During retry**: progress dot turns amber + small badge `Retry 2/3`. Non-blocking toast `Step "{name}" retrying (attempt 2/3)`.
- **Retry exhausted**: failed step's card shows error text + **"Retry this step"** button. Button calls `startGeneration({ fromStep: stepKey })` which preserves state of earlier completed steps.
- **Silent fallback now visible**: web-search skip → warning toast `Web search unavailable, generating without research context`. Agent fallback → toast `Using preset agents (custom agent generation failed)`.
- **Abort**: distinguish user-initiated abort (silent) vs unexpected `AbortError` (show toast).
i18n keys to add (locales under `messages/`):
- `generation.retrying` (with `{attempt}` `{max}` `{step}`)
- `generation.retryFailed`
- `generation.retryStep` (button label)
- `generation.searchSkipped`
- `generation.agentFallback`
- `generation.stepFailed.{stepKey}` (per-step recovery hint)
### D. Server-side alignment
Outline route (`scene-outlines-stream/route.ts:257-396`) already has retry pattern. Extract to shared helper:
```ts
// lib/generation/server-retry.ts
export async function withModelRetry(
fn: (attempt: number) => Promise,
opts: { max: number; onRetry?: (attempt: number) => void },
): Promise;
```
Apply to:
- `app/api/generate/scene-content/route.ts`
- `app/api/generate/scene-actions/route.ts`
- `app/api/generate/agent-profiles/route.ts`
- `app/api/generate/tts/route.ts`
- `app/api/parse-pdf/route.ts` (where safe)
Server retries cover model timeouts / rate limits. Client retries cover network failures. Don't double-retry: client treats `200 + body error` as non-retryable when server signals exhaustion.
### E. Resume from failed step
`startGeneration({ fromStep?: StepKey })`:
- If `fromStep` set, skip earlier completed steps, reuse their cached state from `previewState.steps[k].result`.
- Each step writes its result to `steps[stepKey].result` on success.
- Manual retry button passes `fromStep: stepKey`.
## Out of scope
- Cross-session resume (browser refresh recovers nothing today, keep that way).
- Partial regeneration of one scene within actions/content step (separate issue).
- Cost/quota error handling beyond surfacing the message.
## Rollout
PR 1: A + B + C (client wrapper, state machine, UI). No server changes. Outline keeps existing server retry; client wrapper just won't double-retry on outline-side `error` event.
PR 2: D (server helper, apply to remaining routes).
PR 3: E (resume-from-step). Depends on PR 1's state machine.
## Open questions
1. Backoff numbers — 300/800/2000ms reasonable? Or align with outline's existing values?
2. For silent-fallback steps (web-search, agent-profiles), should user be allowed to manually retry those too, or accept the fallback?
3. Should retry attempts be persisted in `previewState` for telemetry / postmortem analysis?
4. AbortError UX — keep silent on intentional navigation, or always show "Generation cancelled" toast?
## References
- `app/generation-preview/page.tsx:215-971` — orchestration
- `app/generation-preview/page.tsx:962-967` — catch-all + AbortError swallow
- `app/generation-preview/types.ts:20,41-84` — phases & step enum
- `app/api/generate/scene-outlines-stream/route.ts:257-396` — existing retry pattern
- `app/generation-preview/components/visualizers.tsx` — progress UI components
Contributor guide
Assessment
This issue has not been assessed yet.