{"record":{"id":"7a41a199162fb032","repo":"thedotmack/claude-mem","slug":"cannot-process-observation-generation-job-after-ma","errorCode":null,"errorMessage":"cannot process observation generation job after max_attempts is reached","messagePattern":"cannot process observation generation job after max_attempts is reached","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":411,"sourceCode":"  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,\n    sourceId: row.source_id,\n    serverSessionId: row.server_session_id,\n    jobType: row.job_type,\n    status: row.status,","sourceCodeStart":393,"sourceCodeEnd":429,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/storage/postgres/generation-jobs.ts#L393-L429","documentation":"Thrown by assertValidJobStatusTransition when nextStatus is 'processing' and current.attempts >= current.maxAttempts. The job has exhausted its retry budget; no more processing attempts are permitted. The UPDATE statement also enforces this (attempts < max_attempts), so this guard explains a zero-row update.","triggerScenarios":"Calling transitionStatus({status:'processing'}) after the job already attempted maxAttempts times. Default maxAttempts is 3 unless overridden in create().","commonSituations":"Worker retry loop not checking attempt count; bullmq retries exceeding the DB max_attempts; a long-stuck queued job being picked up after prior failures pushed attempts over the limit.","solutions":["Move the job to 'failed' (allowed from both queued and processing) once attempts reach maxAttempts.","If more retries are genuinely needed, create a new job with a higher maxAttempts.","Have the worker check job.attempts < job.maxAttempts before claiming processing."],"exampleFix":"// before\nawait jobs.transitionStatus({ id, projectId, teamId, status:'processing' });\n// after\nif (job.attempts >= job.maxAttempts) {\n  await jobs.transitionStatus({ id, projectId, teamId, status:'failed', lastError:{ reason:'max_attempts' } });\n  return;\n}\nawait jobs.transitionStatus({ id, projectId, teamId, status:'processing' });","handlingStrategy":"validation","validationCode":"if (job.attempts >= job.maxAttempts) { await jobs.transitionStatus({ id, projectId, teamId, status:'failed', lastError:{reason:'max_attempts'} }); return; }","typeGuard":"function canProcess(job){ return job.status!=='processing' && job.attempts < job.maxAttempts; }","tryCatchPattern":null,"preventionTips":["Bound worker retries by DB max_attempts, not just the queue's retry count.","Fail jobs explicitly when the attempt budget is exhausted."],"tags":["postgres","state-machine","generation-jobs","validation","retry"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}