{"record":{"id":"6acc946bc734ae11","repo":"thedotmack/claude-mem","slug":"illegal-observation-generation-job-transition-from","errorCode":null,"errorMessage":"illegal observation generation job transition from ${current.status} to ${nextStatus}","messagePattern":"illegal observation generation job transition from (.+?) to (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":416,"sourceCode":"\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,\n    projectId: row.project_id,\n    teamId: row.team_id,\n    agentEventId: row.agent_event_id,\n    sourceType: row.source_type,","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/storage/postgres/generation-jobs.ts#L398-L434","documentation":"assertValidJobStatusTransition validates the requested transition against ALLOWED_JOB_TRANSITIONS, a whitelist of legal status paths for observation generation jobs. When the current status does not allow jumping to nextStatus, this error is thrown. It enforces a well-defined lifecycle (e.g. queued -> processing -> completed) rather than arbitrary status changes.","triggerScenarios":"transitionStatus calls like queued -> completed, completed -> queued (on a non-terminal-but-completed variant), processing -> queued depending on the map, or any out-of-order sequence not listed in ALLOWED_JOB_TRANSITIONS.","commonSituations":"Application code trying to short-circuit the lifecycle (skip processing, go straight to completed); custom retry logic setting states out of order; two workers updating status concurrently in conflicting orders; version drift after schema/status enumeration changes.","solutions":["Follow the documented status lifecycle: only transition to statuses in ALLOWED_JOB_TRANSITIONS[current.status]","Read the job's current status first and compute the correct next step","Serialize status updates (single worker / row lock) to avoid conflicting transitions","Check ALLOWED_JOB_TRANSITIONS in the source to confirm which transitions are legal before coding the flow"],"exampleFix":"// before\nawait jobs.transitionStatus(jobId, 'completed'); // job is 'queued'\n// after\nawait jobs.transitionStatus(jobId, 'processing');\nawait jobs.transitionStatus(jobId, 'completed');","handlingStrategy":"validation","validationCode":"const next = allowedNextStatuses[current.status];\nif (!next.includes(wanted)) throw new Error(`illegal transition ${current.status} -> ${wanted}`);","typeGuard":null,"tryCatchPattern":"try {\n  await jobs.transitionStatus(jobId, next);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('illegal observation generation job transition')) {\n    // refetch current status and recompute the correct next step\n    const job = await jobs.getByIdForScope(jobId, projectId, teamId);\n    return recomputeAndTransition(job);\n  }\n  throw err;\n}","preventionTips":["Mirror ALLOWED_JOB_TRANSITIONS client-side and validate before calling","Always read fresh job status (avoid stale caches) before transitioning","Serialize status changes per job to avoid racing workers","Write integration tests covering the full lifecycle"],"tags":["state-machine","postgres","lifecycle"],"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"}