THU-MAIC / THU-MAIC/OpenMAIC

RFC: Per-step retry & error UX in classroom generation preview

Open
#573 1 comment 0 reactions 0 assignees View on GitHub
enhancement
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.