thedotmack/claude-mem · error · Error

cannot retry observation generation job after max_attempts…

Error message

cannot retry observation generation job after max_attempts is reached

What it means

When transitioning a job back to 'queued' (a retry), assertValidJobStatusTransition rejects it if current.attempts >= current.maxAttempts. This is the retry-specific counterpart to the max-attempts processing cap, ensuring exhausted jobs cannot be re-queued indefinitely.

Solutions

  1. Check attempts < maxAttempts before calling retry/transition to 'queued'
  2. Create a fresh generation job rather than retrying an exhausted one
  3. Increase maxAttempts at job creation if more retry headroom is required
  4. Resolve the root cause of prior failures (e.g. provider errors) before any requeue

Example fix

// before
await jobs.retryGenerationJob(jobId, projectId, teamId);
// after
const job = await jobs.getByIdForScope(jobId, projectId, teamId);
if (job.attempts < job.maxAttempts) {
  await jobs.retryGenerationJob(jobId, projectId, teamId);
} else {
  await jobs.create({ ...sameSource, maxAttempts: job.maxAttempts + 1 });
}
Defensive patterns

Strategy: validation

Validate before calling

if (nextStatus === 'queued' && job.attempts >= job.maxAttempts) {
  throw new Error('retry budget exhausted');
}

Try / catch

try {
  await jobs.retryGenerationJob(jobId, projectId, teamId);
} catch (err) {
  if (err instanceof Error && err.message.includes('cannot retry observation generation job after max_attempts')) {
    // surface 'retries exhausted' to the user or spawn a new job
    return notifyRetriesExhausted(jobId);
  }
  throw err;
}

Prevention

When it happens

Trigger: retryGenerationJob or a manual transitionStatus(jobId, 'queued') call on a job that has already consumed all its attempts (attempts >= maxAttempts).

Common situations: User clicking 'retry' in a UI after the job exhausted retries; automated retry handlers ignoring the attempts counter; maxAttempts set to 1 so any single failure permanently blocks requeueing.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/a023ce5aafeded3b. Report an issue: GitHub.

Appendix: source

Thrown at src/storage/postgres/generation-jobs.ts:424

function assertValidJobStatusTransition(
  current: PostgresObservationGenerationJob,
  nextStatus: ObservationGenerationJobStatus
): void {
  if (TERMINAL_JOB_STATUSES.has(current.status)) {
    throw new Error(`cannot transition observation generation job from terminal status ${current.status}`);
  }

  if (!ALLOWED_JOB_TRANSITIONS[current.status].includes(nextStatus)) {
    throw new Error(`illegal observation generation job transition from ${current.status} to ${nextStatus}`);
  }

  if (nextStatus === 'processing' && current.attempts >= current.maxAttempts) {
    throw new Error('cannot process observation generation job after max_attempts is reached');
  }

  if (nextStatus === 'queued' && current.attempts >= current.maxAttempts) {
    throw new Error('cannot retry observation generation job after max_attempts is reached');
  }
}

function mapJobRow(row: JobRow): PostgresObservationGenerationJob {
  return {
    id: row.id,
    projectId: row.project_id,
    teamId: row.team_id,
    agentEventId: row.agent_event_id,
    sourceType: row.source_type,
    sourceId: row.source_id,
    serverSessionId: row.server_session_id,
    jobType: row.job_type,
    status: row.status,
    idempotencyKey: row.idempotency_key,
    bullmqJobId: row.bullmq_job_id,
    attempts: row.attempts,
    maxAttempts: row.max_attempts,

View on GitHub (pinned to d8bc9755e7)