{"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":null,"httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":403,"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":385,"sourceCodeEnd":421,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/storage/postgres/generation-jobs.ts#L385-L421","documentation":"Thrown by assertValidJobStatusTransition (reached from transitionStatus after the UPDATE matches zero rows). The job's current status is in TERMINAL_JOB_STATUSES (completed, failed, cancelled), which all have empty ALLOWED_JOB_TRANSITIONS. Terminal jobs are immutable, so any further transition is rejected.","triggerScenarios":"Calling transitionStatus on a job whose status is already 'completed', 'failed', or 'cancelled'. The UPDATE WHERE clause already excludes terminal statuses, so the function re-reads the row and this guard explains why nothing was applied.","commonSituations":"Duplicate worker acknowledgements racing after a job finished; a retry worker that does not check terminal state; UI replaying a transition button; idempotency logic not short-circuiting on terminal jobs.","solutions":["Before calling transitionStatus, load the job and return early if its status is in {completed, failed, cancelled}.","Make the caller idempotent: treat a terminal status matching the desired outcome as success.","If you genuinely need to redo the work, create a new job (new source/idempotency key) instead of reopening a terminal one."],"exampleFix":"// before\nawait jobs.transitionStatus({ id, projectId, teamId, status:'completed' });\n// after\nconst current = await jobs.getByIdForScope({ id, projectId, teamId });\nif (current && ['completed','failed','cancelled'].includes(current.status)) {\n  return current; // already terminal, nothing to do\n}\nawait jobs.transitionStatus({ id, projectId, teamId, status:'completed' });","handlingStrategy":"validation","validationCode":"const TERMINAL = new Set(['completed','failed','cancelled']);\nconst job = await jobs.getByIdForScope({ id, projectId, teamId });\nif (job && TERMINAL.has(job.status)) return job;","typeGuard":"function isTerminalJob(job) { return new Set(['completed','failed','cancelled']).has(job?.status); }","tryCatchPattern":"try { await jobs.transitionStatus({...}); } catch (e) { if (e.message.startsWith('cannot transition observation generation job from terminal status')) { return; } throw e; }","preventionTips":["Design job callers to be idempotent on terminal state.","Never reopen a terminal job; start a new one."],"tags":["postgres","state-machine","generation-jobs","validation"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}