{"record":{"id":"5d8cad4826f8c53a","repo":"toeverything/AFFiNE","slug":"failed-transcript-tasks-cannot-be-settled","errorCode":null,"errorMessage":"Failed transcript tasks cannot be settled","messagePattern":"Failed transcript tasks cannot be settled","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/copilot/transcript/service.ts","lineNumber":480,"sourceCode":"\n    return { id: task.id, status: AiJobStatus.pending, infos };\n  }\n\n  async retryTask(userId: string, workspaceId: string, taskId: string) {\n    return await this.retry.retryTask(userId, workspaceId, taskId);\n  }\n\n  async settleTask(userId: string, workspaceId: string, taskId: string) {\n    const task = await this.models.copilotTranscriptTask.getWithUser(\n      userId,\n      workspaceId,\n      taskId\n    );\n    if (!task) {\n      throw new CopilotTranscriptionJobNotFound();\n    }\n    if (task.status === 'failed') {\n      throw new BadRequestException(\n        'Failed transcript tasks cannot be settled'\n      );\n    }\n    if (task.status !== 'ready' && task.status !== 'settled') {\n      return null;\n    }\n\n    if (task.status === 'settled') {\n      return taskToJob(task);\n    }\n\n    const settled = await this.models.copilotTranscriptTask.settle(task.id);\n    return taskToJob(settled);\n  }\n\n  async queryTask(\n    userId: string,\n    workspaceId: string,","sourceCodeStart":462,"sourceCodeEnd":498,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/591f874dad30887a80143a061a44bd3ca7ee3299/packages/backend/server/src/plugins/copilot/transcript/service.ts#L462-L498","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Fetch the job and only call settle when status is 'ready' (re-settling 'settled' is also safe)","If status is 'failed', call retryTask instead - after a successful retry the task becomes ready and can be settled","In pollers, re-check status immediately before settling to shrink the race window"],"exampleFix":"// before\nawait service.settleTask(userId, workspaceId, taskId);\n\n// after\nconst job = await service.getTask(userId, workspaceId, taskId);\nif (job?.status === 'failed') {\n  await service.retryTask(userId, workspaceId, taskId);\n} else if (job?.status === 'ready' || job?.status === 'settled') {\n  await service.settleTask(userId, workspaceId, taskId);\n}","handlingStrategy":"validation","validationCode":"const job = await client.getTranscriptTask(workspaceId, taskId);\nif (job.status === 'failed') {\n  await client.retryTranscriptTask(workspaceId, taskId);\n} else if (job.status === 'ready' || job.status === 'settled') {\n  await client.settleTranscriptTask({ workspaceId, taskId });\n} // pending/running: poll again later","typeGuard":"const isSettleable = (status: string): status is 'ready' | 'settled' =>\n  status === 'ready' || status === 'settled';","tryCatchPattern":"try {\n  await settle(taskId);\n} catch (e) {\n  if (e instanceof BadRequestException && /cannot be settled/.test(e.message)) {\n    await retryTask(taskId); // failed task: retry, then settle on ready\n  } else throw e;\n}","preventionTips":["Model the task lifecycle (pending -> ready/failed -> settled) in the client and gate settle on it","Poll status immediately before settling to avoid stale-state races","Show a retry affordance instead of settle for failed jobs in the UI"],"tags":["copilot","transcription","state-machine","settle"],"backgroundTag":"invalid-state-transition","analyzedSha":"591f874dad30887a80143a061a44bd3ca7ee3299","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}