{"record":{"id":"8c762e383ab1e89f","repo":"thedotmack/claude-mem","slug":"cannot-transition-observation-generation-job-from","errorCode":null,"errorMessage":"cannot transition observation generation job from terminal status ${current.status}","messagePattern":"cannot transition observation generation job from terminal status (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":412,"sourceCode":"  return { agentEventId: null, serverSessionId: input.serverSessionId ?? null };\n}\n\nconst TERMINAL_JOB_STATUSES = new Set<ObservationGenerationJobStatus>(['completed', 'failed', 'cancelled']);\n\nconst ALLOWED_JOB_TRANSITIONS: Record<ObservationGenerationJobStatus, readonly ObservationGenerationJobStatus[]> = {\n  queued: ['processing', 'failed', 'cancelled'],\n  processing: ['queued', 'completed', 'failed', 'cancelled'],\n  completed: [],\n  failed: [],\n  cancelled: []\n};\n\nfunction assertValidJobStatusTransition(\n  current: PostgresObservationGenerationJob,\n  nextStatus: ObservationGenerationJobStatus\n): void {\n  if (TERMINAL_JOB_STATUSES.has(current.status)) {\n    throw new Error(`cannot transition observation generation job from terminal status ${current.status}`);\n  }\n\n  if (!ALLOWED_JOB_TRANSITIONS[current.status].includes(nextStatus)) {\n    throw new Error(`illegal observation generation job transition from ${current.status} to ${nextStatus}`);\n  }\n\n  if (nextStatus === 'processing' && current.attempts >= current.maxAttempts) {\n    throw new Error('cannot process observation generation job after max_attempts is reached');\n  }\n\n  if (nextStatus === 'queued' && current.attempts >= current.maxAttempts) {\n    throw new Error('cannot retry observation generation job after max_attempts is reached');\n  }\n}\n\nfunction mapJobRow(row: JobRow): PostgresObservationGenerationJob {\n  return {\n    id: row.id,","sourceCodeStart":394,"sourceCodeEnd":430,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/generation-jobs.ts#L394-L430","documentation":"assertValidJobStatusTransition guards the state machine for observation generation jobs. Once a job reaches a terminal status (in TERMINAL_JOB_STATUSES, e.g. completed/failed/cancelled), no further transitions are permitted. Thrown by transitionStatus when attempting to move a job out of a terminal state.","triggerScenarios":"Calling transitionStatus with a nextStatus (even 'processing', 'queued', or 'failed') on a job whose current status is already terminal — e.g. retrying a cancelled job or double-completing a finished job.","commonSituations":"Duplicate worker completion callbacks racing after the job already succeeded; retry logic firing on failed jobs without resetting them; a second API call to cancel an already-completed job; re-delivered queue messages processed twice.","solutions":["Check the job's current status before calling transitionStatus and skip if it is terminal","Treat terminal-status errors as no-ops in retry/duplicate-processing paths (catch and log)","Create a new generation job instead of trying to revive a terminal one","Add idempotency guards (job version/attempt token) so duplicate updates are detected client-side"],"exampleFix":"// before\nawait jobs.transitionStatus(jobId, 'processing');\n// after\nconst job = await jobs.getByIdForScope(jobId, projectId, teamId);\nif (!TERMINAL_STATUSES.has(job.status)) {\n  await jobs.transitionStatus(jobId, 'processing');\n}","handlingStrategy":"try-catch","validationCode":"if (TERMINAL_JOB_STATUSES.has(job.status)) return; // already terminal, nothing to do","typeGuard":null,"tryCatchPattern":"try {\n  await jobs.transitionStatus(jobId, next);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('cannot transition observation generation job from terminal status')) {\n    return; // idempotent no-op\n  }\n  throw err;\n}","preventionTips":["Check current status before every transition","Make status updates idempotent — terminal transitions should be treated as success no-ops","Debounce/dedupe completion callbacks in workers","Never retry a terminal job; create a new one instead"],"tags":["state-machine","postgres","idempotency"],"backgroundTag":"invalid-state-transition","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}