{"record":{"id":"a023ce5aafeded3b","repo":"thedotmack/claude-mem","slug":"cannot-retry-observation-generation-job-after-max","errorCode":null,"errorMessage":"cannot retry observation generation job after max_attempts is reached","messagePattern":"cannot retry observation generation job after max_attempts is reached","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/storage/postgres/generation-jobs.ts","lineNumber":415,"sourceCode":"\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,\n    idempotencyKey: row.idempotency_key,\n    bullmqJobId: row.bullmq_job_id,\n    attempts: row.attempts,\n    maxAttempts: row.max_attempts,","sourceCodeStart":397,"sourceCodeEnd":433,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/storage/postgres/generation-jobs.ts#L397-L433","documentation":"Thrown by assertValidJobStatusTransition when nextStatus is 'queued' (i.e. a retry/back-to-queue) and current.attempts >= current.maxAttempts. Re-queueing for another attempt is not allowed once the budget is spent. Only reachable from 'processing' -> 'queued'.","triggerScenarios":"Calling transitionStatus({status:'queued'}) on a processing job that already used all attempts (a retry-schedule path). The DB UPDATE blocks it and this guard explains why.","commonSituations":"Backoff scheduler naively re-queuing every failed attempt; transient error handler scheduling retries without checking remaining budget; concurrent retry schedulers.","solutions":["Check attempts < maxAttempts before scheduling a retry (transition to 'queued').","Transition to 'failed' with lastError once the budget is exhausted.","Raise maxAttempts on a new job if more attempts are truly required."],"exampleFix":"// before\nawait jobs.transitionStatus({ id, projectId, teamId, status:'queued', nextAttemptAt: backoffDate });\n// after\nif (job.attempts >= job.maxAttempts) {\n  await jobs.transitionStatus({ id, projectId, teamId, status:'failed', lastError });\n} else {\n  await jobs.transitionStatus({ id, projectId, teamId, status:'queued', nextAttemptAt: backoffDate });\n}","handlingStrategy":"validation","validationCode":"if (job.attempts >= job.maxAttempts) { await jobs.transitionStatus({ id, projectId, teamId, status:'failed', lastError }); return; }","typeGuard":"function canRetry(job){ return job.attempts < job.maxAttempts; }","tryCatchPattern":null,"preventionTips":["Gate every retry schedule on attempts < maxAttempts."],"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"}