toeverything/AFFiNE · error · BadRequestException

Failed transcript tasks cannot be settled

Error message

Failed transcript tasks cannot be settled

What it means

settleTask only settles tasks whose status is 'ready' (or already 'settled', which is idempotent). A task with status 'failed' has no successful transcription result to settle, so the service rejects the call with a plain BadRequestException before touching the database.

Source

Thrown at packages/backend/server/src/plugins/copilot/transcript/service.ts:480

    return { id: task.id, status: AiJobStatus.pending, infos };
  }

  async retryTask(userId: string, workspaceId: string, taskId: string) {
    return await this.retry.retryTask(userId, workspaceId, taskId);
  }

  async settleTask(userId: string, workspaceId: string, taskId: string) {
    const task = await this.models.copilotTranscriptTask.getWithUser(
      userId,
      workspaceId,
      taskId
    );
    if (!task) {
      throw new CopilotTranscriptionJobNotFound();
    }
    if (task.status === 'failed') {
      throw new BadRequestException(
        'Failed transcript tasks cannot be settled'
      );
    }
    if (task.status !== 'ready' && task.status !== 'settled') {
      return null;
    }

    if (task.status === 'settled') {
      return taskToJob(task);
    }

    const settled = await this.models.copilotTranscriptTask.settle(task.id);
    return taskToJob(settled);
  }

  async queryTask(
    userId: string,
    workspaceId: string,

View on GitHub (pinned to 591f874dad)

Solutions

  1. Fetch the job and only call settle when status is 'ready' (re-settling 'settled' is also safe)
  2. If status is 'failed', call retryTask instead - after a successful retry the task becomes ready and can be settled
  3. In pollers, re-check status immediately before settling to shrink the race window

Example fix

// before
await service.settleTask(userId, workspaceId, taskId);

// after
const job = await service.getTask(userId, workspaceId, taskId);
if (job?.status === 'failed') {
  await service.retryTask(userId, workspaceId, taskId);
} else if (job?.status === 'ready' || job?.status === 'settled') {
  await service.settleTask(userId, workspaceId, taskId);
}
Defensive patterns

Strategy: validation

Validate before calling

const job = await client.getTranscriptTask(workspaceId, taskId);
if (job.status === 'failed') {
  await client.retryTranscriptTask(workspaceId, taskId);
} else if (job.status === 'ready' || job.status === 'settled') {
  await client.settleTranscriptTask({ workspaceId, taskId });
} // pending/running: poll again later

Type guard

const isSettleable = (status: string): status is 'ready' | 'settled' =>
  status === 'ready' || status === 'settled';

Try / catch

try {
  await settle(taskId);
} catch (e) {
  if (e instanceof BadRequestException && /cannot be settled/.test(e.message)) {
    await retryTask(taskId); // failed task: retry, then settle on ready
  } else throw e;
}

Prevention

When it happens

Trigger: Calling settle on a task whose dispatch previously errored (status 'failed'), e.g. after a transcription provider failure or native action crash; a poll-then-settle loop that settles whatever task it observes without checking status; racing settle against a task that fails concurrently.

Common situations: Background worker blindly settles every task it polls; UI settle button enabled for failed jobs; retry-and-settle race where the failure lands between the status check and the settle call.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of toeverything/AFFiNE@591f874dad (2026-08-18). Data as JSON: /api/errors/5d8cad4826f8c53a. Report an issue: GitHub.