{"record":{"id":"5587fedb52cafcc5","repo":"jamiepine/voicebox","slug":"generation-must-be-completed-to-regenerate","errorCode":null,"errorMessage":"Generation must be completed to regenerate","messagePattern":"Generation must be completed to regenerate","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"backend/routes/generations.py","lineNumber":200,"sourceCode":"            instruct=gen.instruct,\n            mode=\"retry\",\n        )\n    )\n\n    return models.GenerationResponse.model_validate(gen)\n\n\n@router.post(\n    \"/generate/{generation_id}/regenerate\",\n    response_model=models.GenerationResponse,\n)\nasync def regenerate_generation(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"Re-run TTS with the same parameters and save the result as a new version.\"\"\"\n    gen = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not gen:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n    if (gen.status or \"completed\") != \"completed\":\n        raise HTTPException(status_code=400, detail=\"Generation must be completed to regenerate\")\n\n    gen.status = \"generating\"\n    gen.error = None\n    db.commit()\n    db.refresh(gen)\n\n    task_manager = get_task_manager()\n    task_manager.start_generation(\n        task_id=generation_id,\n        profile_id=gen.profile_id,\n        text=gen.text,\n    )\n\n    version_id = str(uuid.uuid4())\n\n    enqueue_generation(\n        generation_id,\n        run_generation(","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/generations.py#L182-L218","documentation":"Returned as HTTP 400 by POST /generate/{generation_id}/regenerate. The guard `(gen.status or 'completed') != 'completed'` only permits regenerate when status is 'completed' (NULL also defaults to 'completed' and is allowed). Regenerate needs finished audio to re-run and produce a new version; 'failed' or 'generating' rows are rejected.","triggerScenarios":"Regenerating a 'failed' generation (use retry instead); regenerating a 'generating' generation still in flight; regenerating a row with an unexpected custom status value.","commonSituations":"User clicks regenerate on a failed generation instead of retry; double-submit where the first call moved status to 'generating'; custom workflow statuses injected outside the normal state machine; UI showing regenerate for non-completed rows.","solutions":["Only show 'regenerate' when generation.status === 'completed' (or NULL treated as completed); show 'retry' for 'failed'.","After a regenerate call, optimistically set status to 'generating' and disable the button until completion.","Refresh status before the call to catch rows that changed state since page load.","For 'failed' rows, route the user to POST /generate/{id}/retry instead."],"exampleFix":"// before\nregenBtn.onclick = () => post(`/generate/${id}/regenerate`);\n// after\nconst done = generation.status === 'completed' || generation.status == null;\nregenBtn.disabled = !done;\nregenBtn.onclick = async () => {\n  await post(`/generate/${id}/regenerate`);\n  generation.status = 'generating';\n  regenBtn.disabled = true;\n};","handlingStrategy":"validation","validationCode":"status = (gen.status or 'completed')\nif status != 'completed':\n    raise NotRegeneratable(status)  # would 400\n# safe to POST /generate/{id}/regenerate","typeGuard":"def is_regeneratable(gen) -> bool:\n    return (getattr(gen, 'status', None) or 'completed') == 'completed'","tryCatchPattern":"try:\n    client.post(f'/generate/{generation_id}/regenerate')\nexcept HTTPStatusError as e:\n    if e.response.status_code == 400 and 'completed' in e.response.json()['detail']:\n        if current_status(generation_id) == 'failed':\n            client.post(f'/generate/{generation_id}/retry')  # route failed -> retry\n        else:\n            wait_for_status(generation_id, 'completed')\n        return\n    raise","preventionTips":["Only show regenerate for status === 'completed' (or NULL).","Route 'failed' rows to retry, not regenerate.","After a regenerate call, set status 'generating' and disable the button until done.","Refresh status before the call to catch state changes."],"tags":["fastapi","http-400","generate","regenerate","state-machine","status-guard","rest"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}