jamiepine/voicebox · warning · HTTPException

Only active generations can be cancelled

Error message

Only active generations can be cancelled

What it means

Returned by POST /generate/{generation_id}/cancel when the generation row exists but its status is not in the active set ("loading_model", "generating"). The guard reads `(gen.status or "completed")`, so a NULL/empty status is treated as "completed" and is therefore non-cancellable. HTTP 400. The endpoint refuses to send a cancel signal to a job that is already terminal or was never started.

Source

Thrown at backend/routes/generations.py:243

            seed=gen.seed,
            instruct=gen.instruct,
            mode="regenerate",
            version_id=version_id,
        )
    )

    return models.GenerationResponse.model_validate(gen)


@router.post("/generate/{generation_id}/cancel")
async def cancel_generation(generation_id: str, db: Session = Depends(get_db)):
    """Cancel a queued or running generation."""
    gen = db.query(DBGeneration).filter_by(id=generation_id).first()
    if not gen:
        raise HTTPException(status_code=404, detail="Generation not found")

    if (gen.status or "completed") not in ("loading_model", "generating"):
        raise HTTPException(status_code=400, detail="Only active generations can be cancelled")

    cancellation_state = cancel_generation_job(generation_id)
    if cancellation_state is None:
        # Row says active but the worker is no longer tracking it — the gen
        # coroutine exited without writing a terminal status (most often a
        # SQLite lock racing with the failed-status write inside the worker's
        # exception handler). Fail the row here so the user can move on.
        task_manager = get_task_manager()
        task_manager.complete_generation(generation_id)
        await history.update_generation_status(
            generation_id=generation_id,
            status="failed",
            db=db,
            error="Generation orphaned by worker",
        )
        return {"message": "Orphaned generation cleared"}

    if cancellation_state == "queued":

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Disable the cancel button in the UI as soon as the status stream reports completed/failed — do not wait for user reaction.
  2. If the row is stuck "queued" but not cancellable, manually update the status to "failed" in the DB or call the orphan-clear path rather than relying on cancel.
  3. Treat 400 from this endpoint as informational (job already terminal) and refresh the row's status from GET /history/{id}.
  4. For legacy NULL-status rows, run a one-time migration setting status to a terminal value.

Example fix

// before
<button onClick={() => cancel(id)}>Cancel</button>

// after: gate on known active status
<button disabled={!['loading_model','generating'].includes(row.status)}
        onClick={() => cancel(id)}>Cancel</button>
Defensive patterns

Strategy: validation

Validate before calling

// Only call cancel when the row is in an active state
const ACTIVE = new Set(['loading_model', 'generating']);
if (!ACTIVE.has(row.status)) { /* don't call cancel */ return; }
await fetch(`/generate/${id}/cancel`, { method:'POST' });

Type guard

const ACTIVE_STATES = new Set(['loading_model', 'generating']);
function isActiveGeneration(gen) {
  return gen != null && ACTIVE_STATES.has(gen.status);
}

Try / catch

try {
  const res = await fetch(`/generate/${id}/cancel`, { method: 'POST' });
  if (res.status === 400) {
    // already terminal (or queued not cancellable) — refresh status
    await refreshStatus(id);
    return;
  }
} catch (e) { console.error(e); }

Prevention

When it happens

Trigger: Cancelling a generation that already finished (status "completed"), already failed (status "failed"), or whose status column is NULL (treated as completed). Also triggered if a generation is still "queued" but queued is not in the active tuple — note only loading_model/generating are accepted.

Common situations: User double-clicks cancel after the job already completed; the SSE status stream hasn't updated the UI yet so the user clicks cancel on a now-terminal job; legacy rows with NULL status created before the status field was added; cancelling a queued job (status=="queued") which the guard does NOT permit.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/6be9831c97dcfa4b. Report an issue: GitHub.